From ab8af87ad8b5e8769c2b011aa6b43e51b9cc3b8b Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 21 Nov 2023 22:42:46 +0000 Subject: [PATCH 01/84] #1184: initial work for caching support, plus scoped variable --- README.md | 1 + docs/index.md | 2 +- examples/caching.ps1 | 31 ++++ src/Pode.psd1 | 17 ++- src/Private/Caching.ps1 | 76 ++++++++++ src/Private/Context.ps1 | 8 ++ src/Private/Helpers.ps1 | 39 +++++- src/Private/Server.ps1 | 4 + src/Public/Caching.ps1 | 303 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 478 insertions(+), 3 deletions(-) create mode 100644 examples/caching.ps1 create mode 100644 src/Private/Caching.ps1 create mode 100644 src/Public/Caching.ps1 diff --git a/README.md b/README.md index 9ba22b6c6..f22f752a6 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Then navigate to `http://127.0.0.1:8000` in your browser. * Generate/bind self-signed certificates * Secret management support to load secrets from vaults * Support for File Watchers +* In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application ## 📦 Install diff --git a/docs/index.md b/docs/index.md index c5d175bd9..14f0c4acf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,7 +43,7 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * Support for dynamically building Routes from Functions and Modules * Generate/bind self-signed certificates * Secret management support to load secrets from vaults -* Support for File Watchers +* Support for File Watchers* In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application ## 🏢 Companies using Pode diff --git a/examples/caching.ps1 b/examples/caching.ps1 new file mode 100644 index 000000000..423188418 --- /dev/null +++ b/examples/caching.ps1 @@ -0,0 +1,31 @@ +$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) +Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8085 +Start-PodeServer -Threads 3 { + + # listen on localhost:8085 + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + + # log errors + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Set-PodeCacheDefaultTtl -Value 60 + + # get cpu, and cache it + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + if ($null -ne $cache:cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # Write-PodeHost 'here - cached' + return + } + + $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # Write-PodeHost 'here - raw' + } + +} \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index cd0ba8d01..1627f580c 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -400,7 +400,22 @@ 'Use-PodeSemaphore', 'Enter-PodeSemaphore', 'Exit-PodeSemaphore', - 'Clear-PodeSemaphores' + 'Clear-PodeSemaphores', + + # caching + 'Get-PodeCache', + 'Set-PodeCache', + 'Test-PodeCache', + 'Remove-PodeCache', + 'Clear-PodeCache', + 'Add-PodeCacheStorage', + 'Remove-PodeCacheStorage', + 'Get-PodeCacheStorage', + 'Test-PodeCacheStorage', + 'Set-PodeCacheDefaultStorage', + 'Get-PodeCacheDefaultStorage', + 'Set-PodeCacheDefaultTtl', + 'Get-PodeCacheDefaultTtl' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1 new file mode 100644 index 000000000..d5e702b3a --- /dev/null +++ b/src/Private/Caching.ps1 @@ -0,0 +1,76 @@ +function Get-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [switch] + $Metadata + ) + + $meta = $PodeContext.Server.Cache.Items[$Name] + if ($null -eq $meta) { + return $null + } + + # check ttl/expiry + if ($meta.Expiry -lt [datetime]::UtcNow) { + Remove-PodeCache -Name $Name + return $null + } + + # return value an metadata if required + if ($Metadata) { + return $meta + } + + # return just the value as default + return $meta.Value +} + +function Set-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [object] + $InputObject, + + [Parameter()] + [int] + $Ttl = 0 + ) + + # crete (or update) value value + $PodeContext.Server.Cache.Items[$Name] = @{ + Value = $InputObject + Ttl = $Ttl + Expiry = [datetime]::UtcNow.AddSeconds($Ttl) + } +} + +function Test-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Cache.Items.ContainsKey($Name) +} + +function Remove-PodeCacheInternal { + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $null = $PodeContext.Server.Cache.Items.Remove($Name) +} + +function Clear-PodeCacheInternal { + $null = $PodeContext.Server.Cache.Items.Clear() +} \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index cacdb4cbe..d5ac517fd 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -254,6 +254,14 @@ function New-PodeContext { # shared state between runspaces $ctx.Server.State = @{} + # setup caching + $ctx.Server.Cache = @{ + Items = @{} + Storage = @{} + DefaultStorage = $null + DefaultTtl = 3600 # 1hr + } + # output details, like variables, to be set once the server stops $ctx.Server.Output = @{ Variables = @{} diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index b9805ace1..a2fa9c23c 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2435,7 +2435,7 @@ function Convert-PodeScopedVariables { $PSSession, [Parameter()] - [ValidateSet('State', 'Session', 'Secret', 'Using')] + [ValidateSet('State', 'Session', 'Secret', 'Using', 'Cache')] $Skip ) @@ -2467,6 +2467,10 @@ function Convert-PodeScopedVariables { $ScriptBlock = Invoke-PodeSecretScriptConversion -ScriptBlock $ScriptBlock } + if (($null -eq $Skip) -or ($Skip -inotcontains 'Cache')) { + $ScriptBlock = Invoke-PodeCacheScriptConversion -ScriptBlock $ScriptBlock + } + # return if (($null -ne $Skip) -and ($Skip -icontains 'Using')) { return $ScriptBlock @@ -2542,6 +2546,39 @@ function Invoke-PodeSecretScriptConversion { return $ScriptBlock } +function Invoke-PodeCacheScriptConversion { + param( + [Parameter()] + [scriptblock] + $ScriptBlock + ) + + # do nothing if no script + if ($null -eq $ScriptBlock) { + return $ScriptBlock + } + + # rename any $secret: vars + $scriptStr = "$($ScriptBlock)" + $found = $false + + while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+)\s*=)') { + $found = $true + $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Name '$($Matches['name'])' -InputObject ") + } + + while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+))') { + $found = $true + $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Name '$($Matches['name'])')") + } + + if ($found) { + $ScriptBlock = [scriptblock]::Create($scriptStr) + } + + return $ScriptBlock +} + function Invoke-PodeSessionScriptConversion { param( [Parameter()] diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 295a62c25..4c7b0fcfb 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -242,6 +242,10 @@ function Restart-PodeInternalServer { # clear up shared state $PodeContext.Server.State.Clear() + # clear cache + $PodeContext.Server.Cache.Items.Clear() + $PodeContext.Server.Cache.Storage.Clear() + # clear up secret vaults/cache Unregister-PodeSecretVaults -ThrowError $PodeContext.Server.Secrets.Vaults.Clear() diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 new file mode 100644 index 000000000..3dce44af4 --- /dev/null +++ b/src/Public/Caching.ps1 @@ -0,0 +1,303 @@ +#TODO: do we need a housekeeping timer? +#TODO: test support for custom storage in get/set + + +function Get-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Storage = $null, + + [switch] + $Metadata + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + return (Get-PodeCacheInternal -Name $Name -Metadata:$Metadata) + } + + # used custom storage + if (Test-PodeCacheStorage -Name $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Name, $Metadata.IsPresent) -Splat -Return) + } + + # storage not found! + throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Name)'" +} + +function Set-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [object] + $InputObject, + + [Parameter()] + [int] + $Ttl = 0, + + [Parameter()] + [string] + $Storage = $null + ) + + # use the global settable default here + if ($Ttl -le 0) { + $Ttl = $PodeContext.Server.Cache.DefaultTtl + } + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Set-PodeCacheInternal -Name $Name -InputObject $InputObject -Ttl $Ttl + } + + # used custom storage + elseif (Test-PodeCacheStorage -Name $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $Value, $Ttl) -Splat + } + + # storage not found! + else { + throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Name)'" + } +} + +function Test-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Storage = $null + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + return (Test-PodeCacheInternal -Name $Name) + } + + # used custom storage + if (Test-PodeCacheStorage -Name $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Name) -Splat -Return) + } + + # storage not found! + throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Name)' exists" +} + +function Remove-PodeCache { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string] + $Storage = $null + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Remove-PodeCacheInternal -Name $Name + } + + # used custom storage + elseif (Test-PodeCacheStorage -Name $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Name) -Splat + } + + # storage not found! + else { + throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Name)'" + } +} + +function Clear-PodeCache { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Storage = $null + ) + + # inmem or custom storage? + if ([string]::IsNullOrEmpty($Storage)) { + $Storage = $PodeContext.Server.Cache.DefaultStorage + } + + # use inmem cache + if ([string]::IsNullOrEmpty($Storage)) { + Clear-PodeCacheInternal + } + + # used custom storage + elseif (Test-PodeCacheStorage -Name $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear + } + + # storage not found! + else { + throw "Cache storage with name '$($Storage)' not found when attempting to clear cached" + } +} + +function Add-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Get, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Set, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Remove, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Test, + + [Parameter(Mandatory = $true)] + [scriptblock] + $Clear, + + [switch] + $Default + ) + + # test if storage already exists + if (Test-PodeCacheStorage -Name $Name) { + throw "Cache Storage with name '$($Name) already exists" + } + + # add cache storage + $PodeContext.Server.Cache.Storage[$Name] = @{ + Name = $Name + Get = $Get + Set = $Set + Remove = $Remove + Test = $Test + Clear = $Clear + Default = $Default.IsPresent + } + + # is default storage? + if ($Default) { + $PodeContext.Server.Cache.DefaultStorage = $Name + } +} + +function Remove-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $null = $PodeContext.Server.Cache.Storage.Remove($Name) +} + +function Get-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Cache.Storage[$Name] +} + +function Test-PodeCacheStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Cache.ContainsKey($Name) +} + +function Set-PodeCacheDefaultStorage { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $PodeContext.Server.Cache.DefaultStorage = $Name +} + +function Get-PodeCacheDefaultStorage { + [CmdletBinding()] + param() + + return $PodeContext.Server.Cache.DefaultStorage +} + +function Set-PodeCacheDefaultTtl { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [int] + $Value + ) + + if ($Value -le 0) { + return + } + + $PodeContext.Server.Cache.DefaultTtl = $Value +} + +function Get-PodeCacheDefaultTtl { + [CmdletBinding()] + param() + + return $PodeContext.Server.Cache.DefaultTtl +} \ No newline at end of file From edbb93f0ec576568b630b50db7dd7718ada464c3 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Thu, 23 Nov 2023 22:28:44 +0000 Subject: [PATCH 02/84] #1184: add cache housekeeper, fix custom storage support --- examples/caching.ps1 | 34 ++++++++++- src/Private/Caching.ps1 | 38 +++++++++++- src/Private/Context.ps1 | 1 + src/Private/Server.ps1 | 3 + src/Public/Caching.ps1 | 8 +-- tests/unit/Server.Tests.ps1 | 112 +++++++++++++++++++----------------- 6 files changed, 132 insertions(+), 64 deletions(-) diff --git a/examples/caching.ps1 b/examples/caching.ps1 index 423188418..26ad157a1 100644 --- a/examples/caching.ps1 +++ b/examples/caching.ps1 @@ -15,6 +15,34 @@ Start-PodeServer -Threads 3 { Set-PodeCacheDefaultTtl -Value 60 + $params = @{ + Set = { + param($name, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $name "$($value)" EX $ttl + } + Get = { + param($name, $metadata) + $result = redis-cli -h localhost -p 6379 GET $name + $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { + return $null + } + return $result + } + Test = { + param($name) + $result = redis-cli -h localhost -p 6379 EXISTS $name + return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + } + Remove = { + param($name) + $null = redis-cli -h localhost -p 6379 EXPIRE $name -1 + } + Clear = {} + } + Add-PodeCacheStorage -Name 'Redis' @params + + # get cpu, and cache it Add-PodeRoute -Method Get -Path '/' -ScriptBlock { if ($null -ne $cache:cpu) { @@ -23,8 +51,12 @@ Start-PodeServer -Threads 3 { return } - $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + # $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Start-Sleep -Milliseconds 500 + $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } + # $cpu = (Get-Random -Minimum 1 -Maximum 1000) + # Write-PodeJsonResponse -Value @{ CPU = $cpu } # Write-PodeHost 'here - raw' } diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1 index d5e702b3a..b7bab08b7 100644 --- a/src/Private/Caching.ps1 +++ b/src/Private/Caching.ps1 @@ -15,7 +15,7 @@ function Get-PodeCacheInternal { # check ttl/expiry if ($meta.Expiry -lt [datetime]::UtcNow) { - Remove-PodeCache -Name $Name + Remove-PodeCacheInternal -Name $Name return $null } @@ -68,9 +68,41 @@ function Remove-PodeCacheInternal { $Name ) - $null = $PodeContext.Server.Cache.Items.Remove($Name) + Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock { + $null = $PodeContext.Server.Cache.Items.Remove($Name) + } } function Clear-PodeCacheInternal { - $null = $PodeContext.Server.Cache.Items.Clear() + Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock { + $null = $PodeContext.Server.Cache.Items.Clear() + } +} + +function Start-PodeCacheHousekeeper { + if (![string]::IsNullOrEmpty((Get-PodeCacheDefaultStorage))) { + return + } + + Add-PodeTimer -Name '__pode_cache_housekeeper__' -Interval 10 -ScriptBlock { + $keys = Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -Return -ScriptBlock { + if ($PodeContext.Server.Cache.Items.Count -eq 0) { + return + } + + return $PodeContext.Server.Cache.Items.Keys.Clone() + } + + if (Test-PodeIsEmpty $keys) { + return + } + + $now = [datetime]::UtcNow + + foreach ($key in $keys) { + if ($PodeContext.Server.Cache.Items[$key].Expiry -lt $now) { + Remove-PodeCacheInternal -Name $key + } + } + } } \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index d5ac517fd..73e855bf6 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -404,6 +404,7 @@ function New-PodeContext { # threading locks, etc. $ctx.Threading.Lockables = @{ Global = [hashtable]::Synchronized(@{}) + Cache = [hashtable]::Synchronized(@{}) Custom = @{} } diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 4c7b0fcfb..4dd58d881 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -41,6 +41,9 @@ function Start-PodeInternalServer { # start timer for task housekeeping Start-PodeTaskHousekeeper + # start the cache housekeeper + Start-PodeCacheHousekeeper + # create timer/schedules for auto-restarting New-PodeAutoRestartServer diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 index 3dce44af4..144992679 100644 --- a/src/Public/Caching.ps1 +++ b/src/Public/Caching.ps1 @@ -1,7 +1,3 @@ -#TODO: do we need a housekeeping timer? -#TODO: test support for custom storage in get/set - - function Get-PodeCache { [CmdletBinding()] param( @@ -73,7 +69,7 @@ function Set-PodeCache { # used custom storage elseif (Test-PodeCacheStorage -Name $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $Value, $Ttl) -Splat + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $InputObject, $Ttl) -Splat } # storage not found! @@ -259,7 +255,7 @@ function Test-PodeCacheStorage { $Name ) - return $PodeContext.Server.Cache.ContainsKey($Name) + return $PodeContext.Server.Cache.Storage.ContainsKey($Name) } function Set-PodeCacheDefaultStorage { diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 68d207ecb..2906a3eea 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -3,8 +3,8 @@ $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } $PodeContext = @{ - Server = $null - Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } + Server = $null + Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } RunspacePools = @{} } @@ -98,121 +98,125 @@ Describe 'Restart-PodeInternalServer' { It 'Resetting the server values' { $PodeContext = @{ - Tokens = @{ + Tokens = @{ Cancellation = New-Object System.Threading.CancellationTokenSource - Restart = New-Object System.Threading.CancellationTokenSource + Restart = New-Object System.Threading.CancellationTokenSource } - Server = @{ - Routes = @{ - GET = @{ 'key' = 'value' } + Server = @{ + Routes = @{ + GET = @{ 'key' = 'value' } POST = @{ 'key' = 'value' } } - Handlers = @{ + Handlers = @{ SMTP = @{} } - Verbs = @{ + Verbs = @{ key = @{} } - Logging = @{ + Logging = @{ Types = @{ 'key' = 'value' } } - Middleware = @{ 'key' = 'value' } - Endpoints = @{ 'key' = 'value' } - EndpointsMap = @{ 'key' = 'value' } - Endware = @{ 'key' = 'value' } - ViewEngine = @{ - Type = 'pode' + Middleware = @{ 'key' = 'value' } + Endpoints = @{ 'key' = 'value' } + EndpointsMap = @{ 'key' = 'value' } + Endware = @{ 'key' = 'value' } + ViewEngine = @{ + Type = 'pode' Extension = 'pode' - Script = $null + Script = $null IsDynamic = $true } - Cookies = @{} - Sessions = @{ 'key' = 'value' } + Cookies = @{} + Sessions = @{ 'key' = 'value' } Authentications = @{ Methods = @{ 'key' = 'value' } } - Authorisations = @{ + Authorisations = @{ Methods = @{ 'key' = 'value' } } - State = @{ 'key' = 'value' } - Output = @{ + State = @{ 'key' = 'value' } + Output = @{ Variables = @{ 'key' = 'value' } } - Configuration = @{ 'key' = 'value' } - Sockets = @{ + Configuration = @{ 'key' = 'value' } + Sockets = @{ Listeners = @() - Queues = @{ + Queues = @{ Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new() } } - Signals = @{ + Signals = @{ Listeners = @() - Queues = @{ - Sockets = @{} + Queues = @{ + Sockets = @{} Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new() } } - OpenAPI = @{} - BodyParsers = @{} - AutoImport = @{ - Modules = @{ Exported = @() } - Snapins = @{ Exported = @() } - Functions = @{ Exported = @() } - SecretVaults = @{ + OpenAPI = @{} + BodyParsers = @{} + AutoImport = @{ + Modules = @{ Exported = @() } + Snapins = @{ Exported = @() } + Functions = @{ Exported = @() } + SecretVaults = @{ SecretManagement = @{ Exported = @() } } } - Views = @{ 'key' = 'value' } - Events = @{ + Views = @{ 'key' = 'value' } + Events = @{ Start = @{} } - Modules = @{} - Security = @{ + Modules = @{} + Security = @{ Headers = @{} - Cache = @{ - ContentSecurity = @{} + Cache = @{ + ContentSecurity = @{} PermissionsPolicy = @{} } } - Secrets = @{ + Secrets = @{ Vaults = @{} - Keys = @{} + Keys = @{} + } + Cache = @{ + Items = @{} + Storage = @{} } } - Metrics = @{ + Metrics = @{ Server = @{ RestartCount = 0 } } - Timers = @{ + Timers = @{ Enabled = $true - Items = @{ + Items = @{ key = 'value' } } Schedules = @{ - Enabled = $true - Items = @{ + Enabled = $true + Items = @{ key = 'value' } Processes = @{} } - Tasks = @{ + Tasks = @{ Enabled = $true - Items = @{ + Items = @{ key = 'value' } Results = @{} } - Fim = @{ + Fim = @{ Enabled = $true - Items = @{ + Items = @{ key = 'value' } } Threading = @{ - Lockables = @{ Custom = @{} } - Mutexes = @{} + Lockables = @{ Custom = @{} } + Mutexes = @{} Semaphores = @{} } } From b6b3285133700303233b7861d677cc0e026a666a Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 28 Nov 2023 22:39:01 +0000 Subject: [PATCH 03/84] #1184: add function summaries, and caching docs --- docs/Tutorials/Caching.md | 153 ++++++++++++++++++++ examples/caching.ps1 | 18 +-- src/Private/Caching.ps1 | 35 +++-- src/Private/Helpers.ps1 | 4 +- src/Public/Caching.ps1 | 277 +++++++++++++++++++++++++++++++++--- tests/unit/Server.Tests.ps1 | 1 + 6 files changed, 446 insertions(+), 42 deletions(-) create mode 100644 docs/Tutorials/Caching.md diff --git a/docs/Tutorials/Caching.md b/docs/Tutorials/Caching.md new file mode 100644 index 000000000..cf7f0338c --- /dev/null +++ b/docs/Tutorials/Caching.md @@ -0,0 +1,153 @@ +# Caching + +Pode has an inbuilt in-memory caching feature, allowing you to cache values for a duration of time to speed up slower queries. You can also setup custom caching storage solutions - such as Redis, and others. + +The default TTL for cached items is 3,600 seconds (1 hour), and this value can be customised either globally or per item. There is also a `$cache:` scoped variable available for use. + +## Caching Items + +To add an item into the cache use [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache), and then to retrieve the value from the cache use [`Get-PodeCache`](../../Functions/Caching/Get-PodeCache). If the item has expired when `Get-PodeCache` is called then `$null` will be returned. + +For example, the following would retrieve the current CPU on Windows machines and cache it for 60 seconds: + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # check cache + $cpu = Get-PodeCache -Key 'cpu' + if ($null -ne $cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cpu } + return + } + + # get cpu, and cache for 60s + $cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + $cpu | Set-PodeCache -Key 'cpu' -Ttl 60 + + Write-PodeJsonResponse -Value @{ CPU = $cpu } +} +``` + +Alternatively, you could use the `$cache:` scoped variable instead. However, using this there is no way to pass the TTL when setting new cached items, so all items cached in this manner will use the default TTL (1 hour, unless changed). Changing the default TTL is discussed [below](#default-ttl). + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # check cache + $cpu = $cache:cpu + if ($null -ne $cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cpu } + return + } + + # get cpu, and cache for 1hr + $cache:cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } +} +``` + +You can test if an item exists in the cache, and isn't expired, using [`Test-PodeCache`](../../Functions/Caching/Test-PodeCache) - this is useful to call if the cached value for a key happens to genuinely be `$null`, you can see if the key actually does exist. + +If you need to invalidate a cached value you can use [`Remove-PodeCache`](../../Functions/Caching/Remove-PodeCache), or if you need to invalidate the whole cache you can use [`Clear-PodeCache`](../../Functions/Caching/Clear-PodeCache). + +### Default TTL + +The default TTL for cached items, when the server starts, is 1 hour. This can be changed by using [`Set-PodeCacheDefaultTtl`](../../Functions/Caching/Set-PodeCacheDefaultTtl). The following updates the default TTL to 60 seconds: + +```powershell +Start-PodeServer { + Set-PodeCacheDefaultTtl -Value 60 +} +``` + +All new cached items will use this TTL by default, unless the one is explicitly specified on [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache) using the `-Ttl` parameter. + +## Custom Storage + +The inbuilt storage used by Pode is a simple in-memory synchronized hashtable, if you're running multiple instances of your Pode server then you'll have multiple caches as well - potentially with different values for the keys. + +You can setup custom storage devices for your cached values using [`Add-PodeCacheStorage`](../../Functions/Caching/Add-PodeCacheStorage) - you can also setup multiple different storages, and specify where certain items should be cached using the `-Storage` parameter on `Get-PodeCache` and `Set-PodeCache`. + +When setting up a new cache storage, you are required to specific a series of scriptblocks for: + +* Setting a cached item (create/update). (`-Set`) +* Getting a cached item's value. (`-Get`) +* Testing if a cached item exists. (`-Test`) +* Removing a cached item. (`-Remove`) +* Clearing a cache of all items. (`-Clear`) + +!!! note + Not all providers will support all options, such as clearing the whole cache. When this is the case simply pass an empty scriptblock to the parameter. + +The `-Test` and `-Remove` scriptblocks will each be supplied the key for cached item; the `-Test` scriptblock should return a boolea value. The `-Set` scriptblock will be supplied the key, value and TTL for the cached item. The `-Get` scriptblock will be supplied the key of the item to retrieve, but also a boolean "metadata" flag - if this metadata is flag is false, just return the item's value, but if it's true return a hashtable of the value and other metadata properties for expiry and ttl. + +For example, say you want to use Redis to store your cached items, then you would have a similar setup to the below: + +```powershell +$params = @{ + Set = { + param($key, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $key "$($value)" EX $ttl + } + Get = { + param($key, $metadata) + $result = redis-cli -h localhost -p 6379 GET $key + $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') + if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { + return $null + } + + if ($metadata) { + $ttl = redis-cli -h localhost -p 6379 TTL $key + $ttl = [int]([System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText')) + + $result = @{ + Value = $result + Ttl = $ttl + Expiry = [datetime]::UtcNow.AddSeconds($ttl) + } + } + + return $result + } + Test = { + param($key) + $result = redis-cli -h localhost -p 6379 EXISTS $key + return ([System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') -eq '1') + } + Remove = { + param($key) + $null = redis-cli -h localhost -p 6379 EXPIRE $key -1 + } + Clear = {} +} + +Add-PodeCacheStorage -Name 'Redis' @params +``` + +And then to use the storage, pass the name to the `-Storage` parameter: + +```powershell +Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + # check cache + $cpu = Get-PodeCache -Key 'cpu' -Storage 'Redis' + if ($null -ne $cpu) { + Write-PodeJsonResponse -Value @{ CPU = $cpu } + return + } + + # get cpu, and cache for 60s + $cpu = (Get-Counter '\Processor(_Total)\% Processor Time').CounterSamples.CookedValue + $cpu | Set-PodeCache -Key 'cpu' -Ttl 60 -Storage 'Redis' + + Write-PodeJsonResponse -Value @{ CPU = $cpu } +} +``` + +### Default Storage + +Similar to the TTL, you can change the default cache storage from Pode's in-memory one to a custom added one. This default storage will be used for all cached items when `-Storage` is supplied, and when using `$cache:` as well. + +```powershell +Start-PodeServer { + Set-PodeCacheDefaultStorage -Name 'Redis' +} +``` diff --git a/examples/caching.ps1 b/examples/caching.ps1 index 26ad157a1..d2dfda012 100644 --- a/examples/caching.ps1 +++ b/examples/caching.ps1 @@ -17,12 +17,12 @@ Start-PodeServer -Threads 3 { $params = @{ Set = { - param($name, $value, $ttl) - $null = redis-cli -h localhost -p 6379 SET $name "$($value)" EX $ttl + param($key, $value, $ttl) + $null = redis-cli -h localhost -p 6379 SET $key "$($value)" EX $ttl } Get = { - param($name, $metadata) - $result = redis-cli -h localhost -p 6379 GET $name + param($key, $metadata) + $result = redis-cli -h localhost -p 6379 GET $key $result = [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') if ([string]::IsNullOrEmpty($result) -or ($result -ieq '(nil)')) { return $null @@ -30,13 +30,13 @@ Start-PodeServer -Threads 3 { return $result } Test = { - param($name) - $result = redis-cli -h localhost -p 6379 EXISTS $name + param($key) + $result = redis-cli -h localhost -p 6379 EXISTS $key return [System.Management.Automation.Internal.StringDecorated]::new($result).ToString('PlainText') } Remove = { - param($name) - $null = redis-cli -h localhost -p 6379 EXPIRE $name -1 + param($key) + $null = redis-cli -h localhost -p 6379 EXPIRE $key -1 } Clear = {} } @@ -45,7 +45,7 @@ Start-PodeServer -Threads 3 { # get cpu, and cache it Add-PodeRoute -Method Get -Path '/' -ScriptBlock { - if ($null -ne $cache:cpu) { + if ((Test-PodeCache -Key 'cpu') -and ($null -ne $cache:cpu)) { Write-PodeJsonResponse -Value @{ CPU = $cache:cpu } # Write-PodeHost 'here - cached' return diff --git a/src/Private/Caching.ps1 b/src/Private/Caching.ps1 index b7bab08b7..30c76e50b 100644 --- a/src/Private/Caching.ps1 +++ b/src/Private/Caching.ps1 @@ -2,20 +2,20 @@ function Get-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [switch] $Metadata ) - $meta = $PodeContext.Server.Cache.Items[$Name] + $meta = $PodeContext.Server.Cache.Items[$Key] if ($null -eq $meta) { return $null } # check ttl/expiry if ($meta.Expiry -lt [datetime]::UtcNow) { - Remove-PodeCacheInternal -Name $Name + Remove-PodeCacheInternal -Key $Key return $null } @@ -32,7 +32,7 @@ function Set-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter(Mandatory = $true)] [object] @@ -44,7 +44,7 @@ function Set-PodeCacheInternal { ) # crete (or update) value value - $PodeContext.Server.Cache.Items[$Name] = @{ + $PodeContext.Server.Cache.Items[$Key] = @{ Value = $InputObject Ttl = $Ttl Expiry = [datetime]::UtcNow.AddSeconds($Ttl) @@ -55,21 +55,36 @@ function Test-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name + $Key ) - return $PodeContext.Server.Cache.Items.ContainsKey($Name) + # if it's not in the cache at all, return false + if (!$PodeContext.Server.Cache.Items.ContainsKey($Key)) { + return $false + } + + # fetch the items metadata, and check expiry. If it's expired return false. + $meta = $PodeContext.Server.Cache.Items[$Key] + + # check ttl/expiry + if ($meta.Expiry -lt [datetime]::UtcNow) { + Remove-PodeCacheInternal -Key $Key + return $false + } + + # it exists, and isn't expired + return $true } function Remove-PodeCacheInternal { param( [Parameter(Mandatory = $true)] [string] - $Name + $Key ) Lock-PodeObject -Object $PodeContext.Threading.Lockables.Cache -ScriptBlock { - $null = $PodeContext.Server.Cache.Items.Remove($Name) + $null = $PodeContext.Server.Cache.Items.Remove($Key) } } @@ -101,7 +116,7 @@ function Start-PodeCacheHousekeeper { foreach ($key in $keys) { if ($PodeContext.Server.Cache.Items[$key].Expiry -lt $now) { - Remove-PodeCacheInternal -Name $key + Remove-PodeCacheInternal -Key $key } } } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index a2fa9c23c..9663380c4 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2564,12 +2564,12 @@ function Invoke-PodeCacheScriptConversion { while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+)\s*=)') { $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Name '$($Matches['name'])' -InputObject ") + $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Key '$($Matches['name'])' -InputObject ") } while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+))') { $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Name '$($Matches['name'])')") + $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Key '$($Matches['name'])')") } if ($found) { diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 index 144992679..df7972b19 100644 --- a/src/Public/Caching.ps1 +++ b/src/Public/Caching.ps1 @@ -1,9 +1,37 @@ +<# +.SYNOPSIS +Return the value of a key from the cache. You can use "$value = $cache:key" as well. + +.DESCRIPTION +Return the value of a key from the cache, or returns the value plus metadata such as expiry time if required. You can use "$value = $cache:key" as well. + +.PARAMETER Key +The Key to be retrieved. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.PARAMETER Metadata +If supplied, and if supported by the cache storage, an metadata such as expiry times will also be returned. + +.EXAMPLE +$value = Get-PodeCache -Key 'ExampleKey' + +.EXAMPLE +$value = Get-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage' + +.EXAMPLE +$value = Get-PodeCache -Key 'ExampleKey' -Metadata + +.EXAMPLE +$value = $cache:ExampleKey +#> function Get-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter()] [string] @@ -20,24 +48,61 @@ function Get-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - return (Get-PodeCacheInternal -Name $Name -Metadata:$Metadata) + return (Get-PodeCacheInternal -Key $Key -Metadata:$Metadata) } # used custom storage - if (Test-PodeCacheStorage -Name $Storage) { - return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Name, $Metadata.IsPresent) -Splat -Return) + if (Test-PodeCacheStorage -Key $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Get -Arguments @($Key, $Metadata.IsPresent) -Splat -Return) } # storage not found! - throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Name)'" + throw "Cache storage with name '$($Storage)' not found when attempting to retrieve cached item '$($Key)'" } +<# +.SYNOPSIS +Set (create/update) a key in the cache. You can use "$cache:key = 'value'" as well. + +.DESCRIPTION +Set (create/update) a key in the cache, with an optional TTL value. You can use "$cache:key = 'value'" as well. + +.PARAMETER Key +The Key to be set. + +.PARAMETER InputObject +The value of the key to be set, can be any object type. + +.PARAMETER Ttl +An optional TTL value, in seconds. The default is whatever "Get-PodeCacheDefaultTtl" retuns, which will be 3600 seconds when not set. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Storage 'ExampleStorage' + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject 'ExampleValue' -Ttl 300 + +.EXAMPLE +Set-PodeCache -Key 'ExampleKey' -InputObject @{ Value = 'ExampleValue' } + +.EXAMPLE +@{ Value = 'ExampleValue' } | Set-PodeCache -Key 'ExampleKey' + +.EXAMPLE +$cache:ExampleKey = 'ExampleValue' +#> function Set-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] @@ -64,26 +129,45 @@ function Set-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - Set-PodeCacheInternal -Name $Name -InputObject $InputObject -Ttl $Ttl + Set-PodeCacheInternal -Key $Key -InputObject $InputObject -Ttl $Ttl } # used custom storage - elseif (Test-PodeCacheStorage -Name $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Name, $InputObject, $Ttl) -Splat + elseif (Test-PodeCacheStorage -Key $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat } # storage not found! else { - throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Name)'" + throw "Cache storage with name '$($Storage)' not found when attempting to set cached item '$($Key)'" } } +<# +.SYNOPSIS +Test if a key exists in the cache. + +.DESCRIPTION +Test if a key exists in the cache, and isn't expired. + +.PARAMETER Key +The Key to test. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Test-PodeCache -Key 'ExampleKey' + +.EXAMPLE +Test-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage' +#> function Test-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter()] [string] @@ -97,24 +181,43 @@ function Test-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - return (Test-PodeCacheInternal -Name $Name) + return (Test-PodeCacheInternal -Key $Key) } # used custom storage - if (Test-PodeCacheStorage -Name $Storage) { - return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Name) -Splat -Return) + if (Test-PodeCacheStorage -Key $Storage) { + return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Test -Arguments @($Key) -Splat -Return) } # storage not found! - throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Name)' exists" + throw "Cache storage with name '$($Storage)' not found when attempting to check if cached item '$($Key)' exists" } +<# +.SYNOPSIS +Remove a key from the cache. + +.DESCRIPTION +Remove a key from the cache. + +.PARAMETER Key +The Key to be removed. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Remove-PodeCache -Key 'ExampleKey' + +.EXAMPLE +Remove-PodeCache -Key 'ExampleKey' -Storage 'ExampleStorage' +#> function Remove-PodeCache { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] - $Name, + $Key, [Parameter()] [string] @@ -128,20 +231,36 @@ function Remove-PodeCache { # use inmem cache if ([string]::IsNullOrEmpty($Storage)) { - Remove-PodeCacheInternal -Name $Name + Remove-PodeCacheInternal -Key $Key } # used custom storage - elseif (Test-PodeCacheStorage -Name $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Name) -Splat + elseif (Test-PodeCacheStorage -Key $Storage) { + Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat } # storage not found! else { - throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Name)'" + throw "Cache storage with name '$($Storage)' not found when attempting to remove cached item '$($Key)'" } } +<# +.SYNOPSIS +Clear all keys from the cache. + +.DESCRIPTION +Clear all keys from the cache. + +.PARAMETER Storage +An optional cache Storage name. (Default: in-memory) + +.EXAMPLE +Clear-PodeCache + +.EXAMPLE +Clear-PodeCache -Storage 'ExampleStorage' +#> function Clear-PodeCache { [CmdletBinding()] param( @@ -161,7 +280,7 @@ function Clear-PodeCache { } # used custom storage - elseif (Test-PodeCacheStorage -Name $Storage) { + elseif (Test-PodeCacheStorage -Key $Storage) { Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear } @@ -171,6 +290,37 @@ function Clear-PodeCache { } } +<# +.SYNOPSIS +Add a cache storage. + +.DESCRIPTION +Add a cache storage. + +.PARAMETER Name +The Name of the cache storage. + +.PARAMETER Get +A Get ScriptBlock, to retrieve a key's value from the cache, or the value plus metadata if required. Supplied parameters: Key, Metadata. + +.PARAMETER Set +A Set ScriptBlock, to set/create/update a key's value in the cache. Supplied parameters: Key, Value, TTL. + +.PARAMETER Remove +A Remove ScriptBlock, to remove a key from the cache. Supplied parameters: Key. + +.PARAMETER Test +A Test ScriptBlock, to test if a key exists in the cache. Supplied parameters: Key. + +.PARAMETER Clear +A Clear ScriptBlock, to remove all keys from the cache. Use an empty ScriptBlock if not supported. + +.PARAMETER Default +If supplied, this cache storage will be set as the default storage. + +.EXAMPLE +Add-PodeCacheStorage -Name 'ExampleStorage' -Get {} -Set {} -Remove {} -Test {} -Clear {} +#> function Add-PodeCacheStorage { [CmdletBinding()] param( @@ -225,6 +375,19 @@ function Add-PodeCacheStorage { } } +<# +.SYNOPSIS +Remove a cache storage. + +.DESCRIPTION +Remove a cache storage. + +.PARAMETER Name +The Name of the cache storage. + +.EXAMPLE +Remove-PodeCacheStorage -Name 'ExampleStorage' +#> function Remove-PodeCacheStorage { [CmdletBinding()] param( @@ -236,6 +399,19 @@ function Remove-PodeCacheStorage { $null = $PodeContext.Server.Cache.Storage.Remove($Name) } +<# +.SYNOPSIS +Returns a cache storage. + +.DESCRIPTION +Returns a cache storage. + +.PARAMETER Name +The Name of the cache storage. + +.EXAMPLE +$storage = Get-PodeCacheStorage -Name 'ExampleStorage' +#> function Get-PodeCacheStorage { [CmdletBinding()] param( @@ -247,6 +423,19 @@ function Get-PodeCacheStorage { return $PodeContext.Server.Cache.Storage[$Name] } +<# +.SYNOPSIS +Test if a cache storage has been added/exists. + +.DESCRIPTION +Test if a cache storage has been added/exists. + +.PARAMETER Name +The Name of the cache storage. + +.EXAMPLE +if (Test-PodeCacheStorage -Name 'ExampleStorage') { } +#> function Test-PodeCacheStorage { [CmdletBinding()] param( @@ -258,6 +447,19 @@ function Test-PodeCacheStorage { return $PodeContext.Server.Cache.Storage.ContainsKey($Name) } +<# +.SYNOPSIS +Set a default cache storage. + +.DESCRIPTION +Set a default cache storage. + +.PARAMETER Name +The Name of the default storage to use for caching. + +.EXAMPLE +Set-PodeCacheDefaultStorage -Name 'ExampleStorage' +#> function Set-PodeCacheDefaultStorage { [CmdletBinding()] param( @@ -269,6 +471,16 @@ function Set-PodeCacheDefaultStorage { $PodeContext.Server.Cache.DefaultStorage = $Name } +<# +.SYNOPSIS +Returns the current default cache Storage name. + +.DESCRIPTION +Returns the current default cache Storage name. Empty/null if one isn't set. + +.EXAMPLE +$storageName = Get-PodeCacheDefaultStorage +#> function Get-PodeCacheDefaultStorage { [CmdletBinding()] param() @@ -276,6 +488,19 @@ function Get-PodeCacheDefaultStorage { return $PodeContext.Server.Cache.DefaultStorage } +<# +.SYNOPSIS +Set a default cache TTL. + +.DESCRIPTION +Set a default cache TTL. + +.PARAMETER Value +A default TTL value, in seconds, to use when setting cache key expiries. + +.EXAMPLE +Set-PodeCacheDefaultTtl -Value 3600 +#> function Set-PodeCacheDefaultTtl { [CmdletBinding()] param( @@ -291,6 +516,16 @@ function Set-PodeCacheDefaultTtl { $PodeContext.Server.Cache.DefaultTtl = $Value } +<# +.SYNOPSIS +Returns the current default cache TTL value. + +.DESCRIPTION +Returns the current default cache TTL value. 3600 seconds is the default TTL if not set. + +.EXAMPLE +$ttl = Get-PodeCacheDefaultTtl +#> function Get-PodeCacheDefaultTtl { [CmdletBinding()] param() diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 2906a3eea..f9da5c770 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -26,6 +26,7 @@ Describe 'Start-PodeInternalServer' { Mock Import-PodeModulesIntoRunspaceState { } Mock Import-PodeSnapinsIntoRunspaceState { } Mock Import-PodeFunctionsIntoRunspaceState { } + Mock Start-PodeCacheHousekeeper { } Mock Invoke-PodeEvent { } Mock Write-Verbose { } From e2b112a2df061ecde61e20be72ada0764f1d3f1f Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 10 Dec 2023 22:47:39 +0000 Subject: [PATCH 04/84] #1184: tweaks for caching docs --- docs/Tutorials/Caching.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/Tutorials/Caching.md b/docs/Tutorials/Caching.md index cf7f0338c..3c5904b30 100644 --- a/docs/Tutorials/Caching.md +++ b/docs/Tutorials/Caching.md @@ -1,12 +1,12 @@ # Caching -Pode has an inbuilt in-memory caching feature, allowing you to cache values for a duration of time to speed up slower queries. You can also setup custom caching storage solutions - such as Redis, and others. +Pode has an inbuilt in-memory caching feature, allowing you to cache values for a duration of time to speed up slower queries. You can also set up custom caching storage solutions - such as Redis, and others. The default TTL for cached items is 3,600 seconds (1 hour), and this value can be customised either globally or per item. There is also a `$cache:` scoped variable available for use. ## Caching Items -To add an item into the cache use [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache), and then to retrieve the value from the cache use [`Get-PodeCache`](../../Functions/Caching/Get-PodeCache). If the item has expired when `Get-PodeCache` is called then `$null` will be returned. +To add an item to the cache use [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache), and then to retrieve the value from the cache use [`Get-PodeCache`](../../Functions/Caching/Get-PodeCache). If the item has expired when `Get-PodeCache` is called then `$null` will be returned. For example, the following would retrieve the current CPU on Windows machines and cache it for 60 seconds: @@ -44,7 +44,7 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { } ``` -You can test if an item exists in the cache, and isn't expired, using [`Test-PodeCache`](../../Functions/Caching/Test-PodeCache) - this is useful to call if the cached value for a key happens to genuinely be `$null`, you can see if the key actually does exist. +You can test if an item exists in the cache, and isn't expired, using [`Test-PodeCache`](../../Functions/Caching/Test-PodeCache) - this is useful to call if the cached value for a key happens to genuinely be `$null`, so you can see if the key does exist. If you need to invalidate a cached value you can use [`Remove-PodeCache`](../../Functions/Caching/Remove-PodeCache), or if you need to invalidate the whole cache you can use [`Clear-PodeCache`](../../Functions/Caching/Clear-PodeCache). @@ -58,13 +58,13 @@ Start-PodeServer { } ``` -All new cached items will use this TTL by default, unless the one is explicitly specified on [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache) using the `-Ttl` parameter. +All new cached items will use this TTL by default unless the one is explicitly specified on [`Set-PodeCache`](../../Functions/Caching/Set-PodeCache) using the `-Ttl` parameter. ## Custom Storage The inbuilt storage used by Pode is a simple in-memory synchronized hashtable, if you're running multiple instances of your Pode server then you'll have multiple caches as well - potentially with different values for the keys. -You can setup custom storage devices for your cached values using [`Add-PodeCacheStorage`](../../Functions/Caching/Add-PodeCacheStorage) - you can also setup multiple different storages, and specify where certain items should be cached using the `-Storage` parameter on `Get-PodeCache` and `Set-PodeCache`. +You can set up custom storage devices for your cached values using [`Add-PodeCacheStorage`](../../Functions/Caching/Add-PodeCacheStorage) - you can also set up multiple different storages, and specify where certain items should be cached using the `-Storage` parameter on `Get-PodeCache` and `Set-PodeCache`. When setting up a new cache storage, you are required to specific a series of scriptblocks for: @@ -77,9 +77,9 @@ When setting up a new cache storage, you are required to specific a series of sc !!! note Not all providers will support all options, such as clearing the whole cache. When this is the case simply pass an empty scriptblock to the parameter. -The `-Test` and `-Remove` scriptblocks will each be supplied the key for cached item; the `-Test` scriptblock should return a boolea value. The `-Set` scriptblock will be supplied the key, value and TTL for the cached item. The `-Get` scriptblock will be supplied the key of the item to retrieve, but also a boolean "metadata" flag - if this metadata is flag is false, just return the item's value, but if it's true return a hashtable of the value and other metadata properties for expiry and ttl. +The `-Test` and `-Remove` scriptblocks will each be supplied the key for the cached item; the `-Test` scriptblock should return a boolean value. The `-Set` scriptblock will be supplied with the key, value, and TTL for the cached item. The `-Get` scriptblock will be supplied with the key of the item to retrieve, but also a boolean "metadata" flag - if this metadata flag is false, just return the item's value, but if it's true return a hashtable of the value and other metadata properties for expiry and ttl. -For example, say you want to use Redis to store your cached items, then you would have a similar setup to the below: +For example, say you want to use Redis to store your cached items, then you would have a similar setup to the one below. ```powershell $params = @{ @@ -123,7 +123,7 @@ $params = @{ Add-PodeCacheStorage -Name 'Redis' @params ``` -And then to use the storage, pass the name to the `-Storage` parameter: +Then to use the storage, pass the name to the `-Storage` parameter: ```powershell Add-PodeRoute -Method Get -Path '/' -ScriptBlock { @@ -144,7 +144,7 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { ### Default Storage -Similar to the TTL, you can change the default cache storage from Pode's in-memory one to a custom added one. This default storage will be used for all cached items when `-Storage` is supplied, and when using `$cache:` as well. +Similar to the TTL, you can change the default cache storage from Pode's in-memory one to a custom-added one. This default storage will be used for all cached items when `-Storage` is supplied, and when using `$cache:` as well. ```powershell Start-PodeServer { From 93a2ed85aa185c2637cd069dcaa1fd0b72c5b04a Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 10 Dec 2023 22:59:31 +0000 Subject: [PATCH 05/84] #1184: docs styling fix --- docs/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 14f0c4acf..201ca92d1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,7 +43,8 @@ Pode is a Cross-Platform framework to create web servers that host REST APIs, We * Support for dynamically building Routes from Functions and Modules * Generate/bind self-signed certificates * Secret management support to load secrets from vaults -* Support for File Watchers* In-memory caching, with optional support for external providers (such as Redis) +* Support for File Watchers +* In-memory caching, with optional support for external providers (such as Redis) * (Windows) Open the hosted server as a desktop application ## 🏢 Companies using Pode From 60b42c66c1f1185ab38923ccfa4c07983eb68ff0 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sat, 16 Dec 2023 15:42:39 +0000 Subject: [PATCH 06/84] minor update to docs to fix links --- docs/Tutorials/Authorisation/Overview.md | 62 ++++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/docs/Tutorials/Authorisation/Overview.md b/docs/Tutorials/Authorisation/Overview.md index fb59aa807..5fe85c4ec 100644 --- a/docs/Tutorials/Authorisation/Overview.md +++ b/docs/Tutorials/Authorisation/Overview.md @@ -1,18 +1,18 @@ # Overview -Authorisation can either be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview), or on it's own for custom scenarios. +Authorisation can either be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview), or on its own for custom scenarios. When used with Authentication, Pode can automatically authorise access to Routes based on Roles; Groups; Scopes; Users; or custom validation logic for you, using the currently authenticated User's details. When authorisation fails Pode will respond with an HTTP 403 status code. With authentication, Pode will set the following properties on the `$WebEvent.Auth` object: -| Name | Description | -| ---- | ----------- | +| Name | Description | +| ------------ | --------------------------------------------------------------------------------------------------------------------------- | | IsAuthorised | This value will be `$true` or `$false` depending on whether or not the authenticated user is authorised to access the Route | ## Create an Access Method -To validate authorisation in Pode you'll first need to create an Access scheme using [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme), and then an Access method using [`Add-PodeAccess`](../../../Functions/Authentication/Add-PodeAccess). At its most simple you'll just need a Name, Type and possibly a Match type. +To validate authorisation in Pode you'll first need to create an Access scheme using [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme), and then an Access method using [`Add-PodeAccess`](../../../Functions/Access/Add-PodeAccess). At its most simple you'll just need a Name, Type, and possibly a Match type. For example, you can create a simple Access method for any of the inbuilt types as follows: @@ -25,15 +25,15 @@ New-PodeAccessScheme -Type User | Add-PodeAccess -Name 'UserExample' ### Match Type -Pode supports 3 inbuilt "Match" types for validating access to resources: One, All and None. The default Match type is One; each of them are applied as follows: +Pode supports 3 inbuilt "Match" types for validating access to resources: One, All, and None. The default Match type is One; each of them is applied as follows: -| Type | Description | -| ---- | ----------- | -| One | If the Source's (ie: User's) access values contain at least one of the Destination's (ie: Route's) access values, then authorisation is granted. | -| All | The Source's access values must contain all of the Destination's access values for authorisation to be granted. | -| None | The Source's access values must contain none of the Destination's access values for authorisation to be granted. | +| Type | Description | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| One | If the Source's (ie: User's) access values contain at least one of the Destination's (ie: Route's) access values, then authorisation is granted. | +| All | The Source's access values must contain all of the Destination's access values for authorisation to be granted. | +| None | The Source's access values must contain none of the Destination's access values for authorisation to be granted. | -For example, to setup an Access method where a User must be in every Group that a Route specifies: +For example, to set an Access method where a User must be in every Group that a Route specifies: ```powershell New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'GroupExample' -Match All @@ -41,20 +41,20 @@ New-PodeAccessScheme -Type Group | Add-PodeAccess -Name 'GroupExample' -Match Al ### User Access Lookup -When using Access methods with Authentication and Routes, Pode will lookup the User's "access values" from the `$WebEvent.Auth.User` object. The property within this object that Pode uses depends on the `-Type` supplied to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme): +When using Access methods with Authentication and Routes, Pode will look up the User's "access values" from the `$WebEvent.Auth.User` object. The property within this object that Pode uses depends on the `-Type` supplied to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme): -| Type | Property | -| ---- | -------- | -| Role | Roles | -| Group | Groups | -| Scope | Scopes | -| User | Username | +| Type | Property | +| ------ | ----------------------------------------------------------------------------------------------------------------------------- | +| Role | Roles | +| Group | Groups | +| Scope | Scopes | +| User | Username | | Custom | n/a - you must supply a `-Path` or `-ScriptBlock` to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme) | -You can override this default lookup in one of two ways, by either supplying a custom property `-Path` or a `-ScriptBlock` for more a more advanced lookup (ie: external sources). +You can override this default lookup in one of two ways, by either supplying a custom property `-Path` or a `-ScriptBlock` for a more advanced lookup (ie: external sources). !!! note - If you're using Access methods in a more adhoc manner via [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess), the `-Path` property does nothing. However, if you don't supply a `-Source` to this function then the `-ScriptBlock` will be invoked. + If you're using Access methods in a more adhoc manner via [`Test-PodeAccess`](../../../Functions/Access/Test-PodeAccess), the `-Path` property does nothing. However, if you don't supply a `-Source` to this function then the `-ScriptBlock` will be invoked. #### Lookup Path @@ -79,10 +79,10 @@ And Pode will retrieve the appropriate data for you. #### Lookup ScriptBlock -If the source access values you require are not stored in the `$WebEvent.Auth.User` object but else where (ie: external source), then you can supply a `-ScriptBlock` on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme). When Pode attempts to retrieve access values for the User, or another Source, this scriptblock will be invoked. +If the source access values you require are not stored in the `$WebEvent.Auth.User` object but elsewhere (ie: external source), then you can supply a `-ScriptBlock` on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme). When Pode attempts to retrieve access values for the User, or another Source, this scriptblock will be invoked. !!! note - When using this scriptblock with Authentication the currently authenticated User will be supplied as the first parameter, followed by the `-ArgumentList` values. When using the Access methods in a more adhoc manner via [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess), just the `-ArgumentList` values are supplied. + When using this scriptblock with Authentication the currently authenticated User will be supplied as the first parameter, followed by the `-ArgumentList` values. When using the Access methods in a more adhoc manner via [`Test-PodeAccess`](../../../Functions/Access/Test-PodeAccess), just the `-ArgumentList` values are supplied. For example, if the Role values you need to retrieve are stored in some SQL database: @@ -112,7 +112,7 @@ By default Pode will perform basic array contains checks, to see if the Source/D For example, if the User has just the Role value `Developer`, and Route has `-Role` values of `Developer` and `QA` supplied, and the `-Match` type is left as `One`, then "if the User Role is contained within the Routes Roles" access is authorised. -However, if you require a more custom/advanced validation logic to be applied, you can supply a `-ScriptBlock` to [`Add-PodeAccess`](../../../Functions/Authentication/Add-PodeAccess). The scriptblock will be supplied with the "Source" access values as the first parameter; the "Destination" access values as the second parameter; and then followed by the `-ArgumentList` values. This scriptblock should return a boolean value: true if authorisation granted, or false otherwise. +However, if you require a more custom/advanced validation logic to be applied, you can supply a `-ScriptBlock` to [`Add-PodeAccess`](../../../Functions/Access/Add-PodeAccess). The scriptblock will be supplied with the "Source" access values as the first parameter; the "Destination" access values as the second parameter; and then followed by the `-ArgumentList` values. This scriptblock should return a boolean value: true if authorisation is granted, or false otherwise. !!! note Supplying a `-ScriptBlock` will override the `-Match` type supplied, as this scriptblock will be used for validation instead of Pode's inbuilt Match logic. @@ -139,11 +139,11 @@ New-PodeAccessScheme -Type Scope | Add-PodeAccess -Name 'ScopeExample' -ScriptBl ## Using with Routes -The Access methods will most commonly be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview). When used together, Pode will automatically validate Route Authorisation for after the Authentication flow. If authorisation fails, an HTTP 403 status code will be returned. +The Access methods will most commonly be used in conjunction with [Authentication](../../Authentication/Overview) and [Routes](../../Routes/Overview). When used together, Pode will automatically validate Route Authorisation after the Authentication flow. If authorisation fails, an HTTP 403 status code will be returned. After creating an Access method as outlined above, you can supply the Access method Name to [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute), and other Route functions, using the `-Access` parameter. -On [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) and [`Add-PodeRouteGroup`](../../../Functions/Routes/Add-PodeRouteGroup) there are also the following parameters: `-Role`, `-Group`, `-Scope`, and `-User`. You can supply one ore more string values to these parameters, depending on which Access method type you're using. +On [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute) and [`Add-PodeRouteGroup`](../../../Functions/Routes/Add-PodeRouteGroup) there are also the following parameters: `-Role`, `-Group`, `-Scope`, and `-User`. You can supply one or more string values to these parameters, depending on which Access method type you're using. For example, to verify access to a Route to authorise only Developer role users: @@ -198,11 +198,11 @@ Invoke-RestMethod -Uri http://localhost:8080/route2 -Method Get -Headers @{ Auth ## Merging -Similar to Authentication methods, you can also merge Access methods using [`Merge-PodeAccess`](../../../Functions/Authentication/Merge-PodeAccess). This allows you to have an access strategy where multiple authorisations are required to pass for a user to be fully authorised, or just one of several possible methods. +Similar to Authentication methods, you can also merge Access methods using [`Merge-PodeAccess`](../../../Functions/Access/Merge-PodeAccess). This allows you to have an access strategy where multiple authorisations are required to pass for a user to be fully authorised, or just one of several possible methods. -When you merge access methods together, it becomes a new access method which you can supply to `-Access` on [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute). By default the merged access method expects just one to pass, but you can state that you require all to pass via the `-Valid` parameter on [`Merge-PodeAccess`](../../../Functions/Authentication/Merge-PodeAccess). +When you merge access methods, it becomes a new access method that you can supply to `-Access` on [`Add-PodeRoute`](../../../Functions/Routes/Add-PodeRoute). By default the merged access method expects just one to pass, but you can state that you require all to pass via the `-Valid` parameter on [`Merge-PodeAccess`](../../../Functions/Access/Merge-PodeAccess). -Using the same example above, we could add Group authorisation to this as well so the Developers have to be in a Software Group, and the Admins in a Operations Group: +Using the same example above, we could add Group authorisation to this as well so the Developers have to be in a Software Group, and the Admins in an Operations Group: ```powershell Start-PodeServer { @@ -248,7 +248,7 @@ Start-PodeServer { ## Custom Access -Pode has inbuilt support for Roles, Groups, Scopes, and Users authorisation on Routes. However, if you need to setup a more Custom authorisation policy on Routes you can create a custom Access scheme by supplying `-Custom` to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme), and add custom access values to a Route using [`Add-PodeAccessCustom`](../../../Functions/Authentication/Add-PodeAccessCustom). +Pode has inbuilt support for Roles, Groups, Scopes, and Users authorisation on Routes. However, if you need to set up a more Custom authorisation policy on Routes you can create a custom Access scheme by supplying `-Custom` to [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme), and adding custom access values to a Route using [`Add-PodeAccessCustom`](../../../Functions/Access/Add-PodeAccessCustom). Custom access values for a User won't be automatically loaded from the authenticated User object, and a `-Path` or `-ScriptBlock` on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme) will be required. @@ -303,7 +303,7 @@ Start-PodeServer { ## Using Adhoc -It is possible to invoke the Access method validation in an adhoc manner, without (or while) using Authentication, using [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess). +It is possible to invoke the Access method validation in an adhoc manner, without (or while) using Authentication, using [`Test-PodeAccess`](../../../Functions/Access/Test-PodeAccess). When using the Access methods outside of Authentication/Routes, the `-Type` doesn't really have any bearing. @@ -332,4 +332,4 @@ Start-PodeServer { } ``` -The `-ArgumentList`, on [`Test-PodeAccess`](../../../Functions/Authentication/Test-PodeAccess), will supply values as the first set of parameters to the `-ScriptBlock` defined on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme). +The `-ArgumentList`, on [`Test-PodeAccess`](../../../Functions/Access/Test-PodeAccess), will supply values as the first set of parameters to the `-ScriptBlock` defined on [`New-PodeAccessScheme`](../../../Functions/Access/New-PodeAccessScheme). From 92e5b6f763a3426de7e22dfcbdd325a3197c6147 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Thu, 21 Dec 2023 21:09:45 +0000 Subject: [PATCH 07/84] #1207: initial work for supporting scoping variables --- examples/caching.ps1 | 3 +- examples/web-pages-using.ps1 | 8 +- src/Pode.psd1 | 12 +- src/Private/Context.ps1 | 3 + src/Private/Helpers.ps1 | 334 -------------------------------- src/Private/ScopedVariables.ps1 | 167 ++++++++++++++++ src/Private/Server.ps1 | 9 +- src/Public/Core.ps1 | 3 - src/Public/ScopedVariables.ps1 | 205 ++++++++++++++++++++ tests/unit/Server.Tests.ps1 | 1 + 10 files changed, 400 insertions(+), 345 deletions(-) create mode 100644 src/Private/ScopedVariables.ps1 create mode 100644 src/Public/ScopedVariables.ps1 diff --git a/examples/caching.ps1 b/examples/caching.ps1 index d2dfda012..36c8a6172 100644 --- a/examples/caching.ps1 +++ b/examples/caching.ps1 @@ -6,7 +6,6 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop # create a server, and start listening on port 8085 Start-PodeServer -Threads 3 { - # listen on localhost:8085 Add-PodeEndpoint -Address * -Port 8090 -Protocol Http @@ -42,6 +41,8 @@ Start-PodeServer -Threads 3 { } Add-PodeCacheStorage -Name 'Redis' @params + # set default value for cache + $cache:cpu = (Get-Random -Minimum 1 -Maximum 1000) # get cpu, and cache it Add-PodeRoute -Method Get -Path '/' -ScriptBlock { diff --git a/examples/web-pages-using.ps1 b/examples/web-pages-using.ps1 index df5babb99..f0b5467e2 100644 --- a/examples/web-pages-using.ps1 +++ b/examples/web-pages-using.ps1 @@ -7,8 +7,7 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop $outerfoo = 'outer-bar' $outer_ken = 'Hello, there' -function Write-MyOuterResponse -{ +function Write-MyOuterResponse { Write-PodeJsonResponse -Value @{ Message = 'From an outer function' } } @@ -30,8 +29,7 @@ Start-PodeServer -Threads 2 { $innerfoo = 'inner-bar' $inner_ken = 'General Kenobi' - function Write-MyInnerResponse - { + function Write-MyInnerResponse { Write-PodeJsonResponse -Value @{ Message = 'From an inner function' } } @@ -40,7 +38,7 @@ Start-PodeServer -Threads 2 { New-PodeMiddleware -ScriptBlock { "M1: $($using:outer_ken) ... $($using:inner_ken)" | Out-Default return $true - } | Add-PodeMiddleware -Name 'TestUsingMiddleware1' + } | Add-PodeMiddleware -Name 'TestUsingMiddleware1' Add-PodeMiddleware -Name 'TestUsingMiddleware2' -ScriptBlock { "M2: $($using:outer_ken) ... $($using:inner_ken)" | Out-Default diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 1627f580c..406158e30 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -415,7 +415,17 @@ 'Set-PodeCacheDefaultStorage', 'Get-PodeCacheDefaultStorage', 'Set-PodeCacheDefaultTtl', - 'Get-PodeCacheDefaultTtl' + 'Get-PodeCacheDefaultTtl', + + # scoped variables + 'Convert-PodeScopedVariables', + 'Convert-PodeScopedVariable', + 'Add-PodeScopedVariable', + 'Remove-PodeScopedVariable', + 'Test-PodeScopedVariable', + 'Clear-PodeScopedVariables', + 'Get-PodeScopedVariable', + 'Use-PodeScopedVariables' ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 73e855bf6..97e6648c4 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -438,6 +438,9 @@ function New-PodeContext { } } + # scoped variables + $ctx.Server.ScopedVariables = [ordered]@{} + # return the new context return $ctx } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 9663380c4..42abd59e1 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -2424,340 +2424,6 @@ function Convert-PodeQueryStringToHashTable { return (ConvertFrom-PodeNameValueToHashTable -Collection $tmpQuery) } -function Convert-PodeScopedVariables { - param( - [Parameter()] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [System.Management.Automation.SessionState] - $PSSession, - - [Parameter()] - [ValidateSet('State', 'Session', 'Secret', 'Using', 'Cache')] - $Skip - ) - - # do nothing if no script - if ($null -eq $ScriptBlock) { - if (($null -ne $Skip) -and ($Skip -icontains 'Using')) { - return $ScriptBlock - } - else { - return @($ScriptBlock, $null) - } - } - - # conversions - $usingVars = $null - if (($null -eq $Skip) -or ($Skip -inotcontains 'Using')) { - $ScriptBlock, $usingVars = Invoke-PodeUsingScriptConversion -ScriptBlock $ScriptBlock -PSSession $PSSession - } - - if (($null -eq $Skip) -or ($Skip -inotcontains 'State')) { - $ScriptBlock = Invoke-PodeStateScriptConversion -ScriptBlock $ScriptBlock - } - - if (($null -eq $Skip) -or ($Skip -inotcontains 'Session')) { - $ScriptBlock = Invoke-PodeSessionScriptConversion -ScriptBlock $ScriptBlock - } - - if (($null -eq $Skip) -or ($Skip -inotcontains 'Secret')) { - $ScriptBlock = Invoke-PodeSecretScriptConversion -ScriptBlock $ScriptBlock - } - - if (($null -eq $Skip) -or ($Skip -inotcontains 'Cache')) { - $ScriptBlock = Invoke-PodeCacheScriptConversion -ScriptBlock $ScriptBlock - } - - # return - if (($null -ne $Skip) -and ($Skip -icontains 'Using')) { - return $ScriptBlock - } - else { - return @($ScriptBlock, $usingVars) - } -} - -function Invoke-PodeStateScriptConversion { - param( - [Parameter()] - [scriptblock] - $ScriptBlock - ) - - # do nothing if no script - if ($null -eq $ScriptBlock) { - return $ScriptBlock - } - - # rename any $state: vars - $scriptStr = "$($ScriptBlock)" - $found = $false - - while ($scriptStr -imatch '(?\$state\:(?[a-z0-9_\?]+)\s*=)') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeState -Name '$($Matches['name'])' -Value ") - } - - while ($scriptStr -imatch '(?\$state\:(?[a-z0-9_\?]+))') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "`$PodeContext.Server.State.'$($Matches['name'])'.Value") - } - - if ($found) { - $ScriptBlock = [scriptblock]::Create($scriptStr) - } - - return $ScriptBlock -} - -function Invoke-PodeSecretScriptConversion { - param( - [Parameter()] - [scriptblock] - $ScriptBlock - ) - - # do nothing if no script - if ($null -eq $ScriptBlock) { - return $ScriptBlock - } - - # rename any $secret: vars - $scriptStr = "$($ScriptBlock)" - $found = $false - - while ($scriptStr -imatch '(?\$secret\:(?[a-z0-9_\?]+)\s*=)') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "Update-PodeSecret -Name '$($Matches['name'])' -InputObject ") - } - - while ($scriptStr -imatch '(?\$secret\:(?[a-z0-9_\?]+))') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeSecret -Name '$($Matches['name'])')") - } - - if ($found) { - $ScriptBlock = [scriptblock]::Create($scriptStr) - } - - return $ScriptBlock -} - -function Invoke-PodeCacheScriptConversion { - param( - [Parameter()] - [scriptblock] - $ScriptBlock - ) - - # do nothing if no script - if ($null -eq $ScriptBlock) { - return $ScriptBlock - } - - # rename any $secret: vars - $scriptStr = "$($ScriptBlock)" - $found = $false - - while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+)\s*=)') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "Set-PodeCache -Key '$($Matches['name'])' -InputObject ") - } - - while ($scriptStr -imatch '(?\$cache\:(?[a-z0-9_\?]+))') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "(Get-PodeCache -Key '$($Matches['name'])')") - } - - if ($found) { - $ScriptBlock = [scriptblock]::Create($scriptStr) - } - - return $ScriptBlock -} - -function Invoke-PodeSessionScriptConversion { - param( - [Parameter()] - [scriptblock] - $ScriptBlock - ) - - # do nothing if no script - if ($null -eq $ScriptBlock) { - return $ScriptBlock - } - - # rename any $session: vars - $scriptStr = "$($ScriptBlock)" - $found = $false - - while ($scriptStr -imatch '(?\$session\:(?[a-z0-9_\?]+))') { - $found = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "`$WebEvent.Session.Data.'$($Matches['name'])'") - } - - if ($found) { - $ScriptBlock = [scriptblock]::Create($scriptStr) - } - - return $ScriptBlock -} - -function Invoke-PodeUsingScriptConversion { - param( - [Parameter()] - [scriptblock] - $ScriptBlock, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.SessionState] - $PSSession - ) - - # do nothing if no script - if ($null -eq $ScriptBlock) { - return @($ScriptBlock, $null) - } - - # rename any __using_ vars for inner timers, etcs - $scriptStr = "$($ScriptBlock)" - $foundInnerUsing = $false - - while ($scriptStr -imatch '(?\$__using_(?[a-z0-9_\?]+))') { - $foundInnerUsing = $true - $scriptStr = $scriptStr.Replace($Matches['full'], "`$using:$($Matches['name'])") - } - - if ($foundInnerUsing) { - $ScriptBlock = [scriptblock]::Create($scriptStr) - } - - # get any using variables - $usingVars = Get-PodeScriptUsingVariables -ScriptBlock $ScriptBlock - if (Test-PodeIsEmpty $usingVars) { - return @($ScriptBlock, $null) - } - - # convert any using vars to use new names - $usingVars = ConvertTo-PodeUsingVariables -UsingVariables $usingVars -PSSession $PSSession - - # now convert the script - $newScriptBlock = ConvertTo-PodeUsingScript -ScriptBlock $ScriptBlock -UsingVariables $usingVars - - # return converted script - return @($newScriptBlock, $usingVars) -} - -function Get-PodeScriptUsingVariables { - param( - [Parameter(Mandatory = $true)] - [scriptblock] - $ScriptBlock - ) - - return $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true) -} - -function ConvertTo-PodeUsingVariables { - param( - [Parameter()] - $UsingVariables, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.SessionState] - $PSSession - ) - - if (Test-PodeIsEmpty $UsingVariables) { - return $null - } - - $mapped = @{} - - foreach ($usingVar in $UsingVariables) { - # var name - $varName = $usingVar.SubExpression.VariablePath.UserPath - - # only retrieve value if new var - if (!$mapped.ContainsKey($varName)) { - # get value, or get __using_ value for child scripts - $value = $PSSession.PSVariable.Get($varName) - if ([string]::IsNullOrEmpty($value)) { - $value = $PSSession.PSVariable.Get("__using_$($varName)") - } - - if ([string]::IsNullOrEmpty($value)) { - throw "Value for `$using:$($varName) could not be found" - } - - # add to mapped - $mapped[$varName] = @{ - OldName = $usingVar.SubExpression.Extent.Text - NewName = "__using_$($varName)" - NewNameWithDollar = "`$__using_$($varName)" - SubExpressions = @() - Value = $value.Value - } - } - - # add the vars sub-expression for replacing later - $mapped[$varName].SubExpressions += $usingVar.SubExpression - } - - return @($mapped.Values) -} - -function ConvertTo-PodeUsingScript { - param( - [Parameter(Mandatory = $true)] - [scriptblock] - $ScriptBlock, - - [Parameter()] - [hashtable[]] - $UsingVariables - ) - - # return original script if no using vars - if (Test-PodeIsEmpty $UsingVariables) { - return $ScriptBlock - } - - $varsList = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]' - $newParams = New-Object System.Collections.ArrayList - - foreach ($usingVar in $UsingVariables) { - foreach ($subExp in $usingVar.SubExpressions) { - $null = $varsList.Add($subExp) - } - } - - $newParams.AddRange(@($UsingVariables.NewNameWithDollar)) - $newParams = ($newParams -join ', ') - $tupleParams = [tuple]::Create($varsList, $newParams) - - $bindingFlags = [System.Reflection.BindingFlags]'Default, NonPublic, Instance' - $_varReplacerMethod = $ScriptBlock.Ast.GetType().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags) - $convertedScriptBlockStr = $_varReplacerMethod.Invoke($ScriptBlock.Ast, @($tupleParams)) - - if (!$ScriptBlock.Ast.ParamBlock) { - $convertedScriptBlockStr = "param($($newParams))`n$($convertedScriptBlockStr)" - } - - $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr) - - if ($convertedScriptBlock.Ast.EndBlock[0].Statements.Extent.Text.StartsWith('$input |')) { - $convertedScriptBlockStr = ($convertedScriptBlockStr -ireplace '\$input \|') - $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr) - } - - return $convertedScriptBlock -} - function Get-PodeDotSourcedFiles { param( [Parameter(Mandatory = $true)] diff --git a/src/Private/ScopedVariables.ps1 b/src/Private/ScopedVariables.ps1 new file mode 100644 index 000000000..0632b246a --- /dev/null +++ b/src/Private/ScopedVariables.ps1 @@ -0,0 +1,167 @@ +function Add-PodeScopedVariablesInbuilt { + Add-PodeScopedVariableInbuiltUsing + Add-PodeScopedVariableInbuiltCache + Add-PodeScopedVariableInbuiltSecret + Add-PodeScopedVariableInbuiltSession + Add-PodeScopedVariableInbuiltState +} + +function Add-PodeScopedVariableInbuiltCache { + Add-PodeScopedVariable -Name 'cache' ` + -SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " ` + -GetReplace "Get-PodeCache -Key '{{name}}'" +} + +function Add-PodeScopedVariableInbuiltSecret { + Add-PodeScopedVariable -Name 'secret' ` + -SetReplace "Update-PodeSecret -Name '{{name}}' -InputObject " ` + -GetReplace "Get-PodeSecret -Name '{{name}}'" +} + +function Add-PodeScopedVariableInbuiltSession { + Add-PodeScopedVariable -Name 'session' ` + -SetReplace "`$WebEvent.Session.Data.'{{name}}'" ` + -GetReplace "`$WebEvent.Session.Data.'{{name}}'" +} + +function Add-PodeScopedVariableInbuiltState { + Add-PodeScopedVariable -Name 'state' ` + -SetReplace "Set-PodeState -Name '{{name}}' -Value " ` + -GetReplace "`$PodeContext.Server.State.'{{name}}'.Value" +} + +function Add-PodeScopedVariableInbuiltUsing { + Add-PodeScopedVariable -Name 'using' -ScriptBlock { + param($ScriptBlock, $PSSession) + + # do nothing if no script or session + if (($null -eq $ScriptBlock) -or ($null -eq $PSSession)) { + return $ScriptBlock, $null + } + + # rename any __using_ vars for inner timers, etcs + $strScriptBlock = "$($ScriptBlock)" + $foundInnerUsing = $false + + while ($strScriptBlock -imatch '(?\$__using_(?[a-z0-9_\?]+))') { + $foundInnerUsing = $true + $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "`$using:$($Matches['name'])") + } + + if ($foundInnerUsing) { + $ScriptBlock = [scriptblock]::Create($strScriptBlock) + } + + # get any using variables + $usingVars = Get-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock + if (($null -eq $usingVars) -or ($usingVars.Count -eq 0)) { + return $ScriptBlock, $null + } + + # convert any using vars to use new names + $usingVars = Find-PodeScopedVariableUsingVariableValues -UsingVariables $usingVars -PSSession $PSSession + + # now convert the script + $newScriptBlock = Convert-PodeScopedVariableUsingVariables -ScriptBlock $ScriptBlock -UsingVariables $usingVars + + # return converted script + return $newScriptBlock, $usingVars + } +} + +function Get-PodeScopedVariableUsingVariables { + param( + [Parameter(Mandatory = $true)] + [scriptblock] + $ScriptBlock + ) + + return $ScriptBlock.Ast.FindAll({ $args[0] -is [System.Management.Automation.Language.UsingExpressionAst] }, $true) +} + +function Find-PodeScopedVariableUsingVariableValues { + param( + [Parameter(Mandatory = $true)] + $UsingVariables, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.SessionState] + $PSSession + ) + + $mapped = @{} + + foreach ($usingVar in $UsingVariables) { + # var name + $varName = $usingVar.SubExpression.VariablePath.UserPath + + # only retrieve value if new var + if (!$mapped.ContainsKey($varName)) { + # get value, or get __using_ value for child scripts + $value = $PSSession.PSVariable.Get($varName) + if ([string]::IsNullOrEmpty($value)) { + $value = $PSSession.PSVariable.Get("__using_$($varName)") + } + + if ([string]::IsNullOrEmpty($value)) { + throw "Value for `$using:$($varName) could not be found" + } + + # add to mapped + $mapped[$varName] = @{ + OldName = $usingVar.SubExpression.Extent.Text + NewName = "__using_$($varName)" + NewNameWithDollar = "`$__using_$($varName)" + SubExpressions = @() + Value = $value.Value + } + } + + # add the vars sub-expression for replacing later + $mapped[$varName].SubExpressions += $usingVar.SubExpression + } + + return @($mapped.Values) +} + +function Convert-PodeScopedVariableUsingVariables { + param( + [Parameter(Mandatory = $true)] + [scriptblock] + $ScriptBlock, + + [Parameter(Mandatory = $true)] + [hashtable[]] + $UsingVariables + ) + + $varsList = New-Object 'System.Collections.Generic.List`1[System.Management.Automation.Language.VariableExpressionAst]' + $newParams = New-Object System.Collections.ArrayList + + foreach ($usingVar in $UsingVariables) { + foreach ($subExp in $usingVar.SubExpressions) { + $null = $varsList.Add($subExp) + } + } + + $null = $newParams.AddRange(@($UsingVariables.NewNameWithDollar)) + $newParams = ($newParams -join ', ') + $tupleParams = [tuple]::Create($varsList, $newParams) + + $bindingFlags = [System.Reflection.BindingFlags]'Default, NonPublic, Instance' + $_varReplacerMethod = $ScriptBlock.Ast.GetType().GetMethod('GetWithInputHandlingForInvokeCommandImpl', $bindingFlags) + $convertedScriptBlockStr = $_varReplacerMethod.Invoke($ScriptBlock.Ast, @($tupleParams)) + + if (!$ScriptBlock.Ast.ParamBlock) { + $convertedScriptBlockStr = "param($($newParams))`n$($convertedScriptBlockStr)" + } + + $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr) + + if ($convertedScriptBlock.Ast.EndBlock[0].Statements.Extent.Text.StartsWith('$input |')) { + $convertedScriptBlockStr = ($convertedScriptBlockStr -ireplace '\$input \|') + $convertedScriptBlock = [scriptblock]::Create($convertedScriptBlockStr) + } + + return $convertedScriptBlock +} \ No newline at end of file diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 4dd58d881..2b140d089 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -11,6 +11,9 @@ function Start-PodeInternalServer { # setup temp drives for internal dirs Add-PodePSInbuiltDrives + # setup inbuilt scoped vars + Add-PodeScopedVariablesInbuilt + # create the shared runspace state New-PodeRunspaceState @@ -26,7 +29,8 @@ function Start-PodeInternalServer { $_script = Convert-PodeFileToScriptBlock -FilePath $PodeContext.Server.LogicPath } - Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure + $_script = Convert-PodeScopedVariables -ScriptBlock $_script -Exclude Session, Using + Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat # load any modules/snapins Import-PodeSnapinsIntoRunspaceState @@ -245,6 +249,9 @@ function Restart-PodeInternalServer { # clear up shared state $PodeContext.Server.State.Clear() + # clear scoped variables + $PodeContext.Server.ScopedVariables.Clear() + # clear cache $PodeContext.Server.Cache.Items.Clear() $PodeContext.Server.Cache.Storage.Clear() diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 86a09c7e4..63449a0cc 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -150,9 +150,6 @@ function Start-PodeServer { $RootPath = Get-PodeRelativePath -Path $RootPath -RootPath $MyInvocation.PSScriptRoot -JoinRoot -Resolve -TestPath } - # check for scoped vars - $ScriptBlock = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -Skip Session, Using - # create main context object $PodeContext = New-PodeContext ` -ScriptBlock $ScriptBlock ` diff --git a/src/Public/ScopedVariables.ps1 b/src/Public/ScopedVariables.ps1 new file mode 100644 index 000000000..6b7725c24 --- /dev/null +++ b/src/Public/ScopedVariables.ps1 @@ -0,0 +1,205 @@ +function Convert-PodeScopedVariables { + [CmdletBinding()] + param( + [Parameter(ValueFromPipeline = $true)] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [System.Management.Automation.SessionState] + $PSSession, + + [Parameter()] + [string[]] + $Exclude + ) + + # do nothing if no scriptblock + if ($null -eq $ScriptBlock) { + return $ScriptBlock + } + + # using vars + $usingVars = $null + + # loop through each defined scoped variable and convert, unless excluded + foreach ($key in $PodeContext.Server.ScopedVariables.Keys) { + # excluded? + if ($Exclude -icontains $key) { + continue + } + + # convert scoped var + $ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name $key -ScriptBlock $ScriptBlock -PSSession $PSSession + + # using vars? + if (($null -ne $otherResults) -and ($key -ieq 'using')) { + $usingVars = $otherResults + } + } + + # return just the scriptblock, or include using vars as well + if ($null -ne $usingVars) { + return $ScriptBlock, $usingVars + } + + return $ScriptBlock +} + +function Convert-PodeScopedVariable { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(ValueFromPipeline = $true)] + [scriptblock] + $ScriptBlock, + + [Parameter()] + [System.Management.Automation.SessionState] + $PSSession + ) + + # do nothing if no scriptblock + if ($null -eq $ScriptBlock) { + return $ScriptBlock + } + + # check if scoped var defined + if (!(Test-PodeScopedVariable -Name $Name)) { + throw "Scoped Variable not found: $($Name)" + } + + # get the scoped var metadata + $scopedVar = $PodeContext.Server.ScopedVariables[$Name] + + # scriptblock or replace? + if ($null -ne $scopedVar.ScriptBlock) { + return (Invoke-PodeScriptBlock -ScriptBlock $scopedVar.ScriptBlock -Arguments $ScriptBlock, $PSSession -Splat -Return -NoNewClosure) + } + + # replace style + else { + # convert scriptblock to string + $strScriptBlock = "$($ScriptBlock)" + + # see if the script contains any form of the scoped variable, and if not just return + $found = $strScriptBlock -imatch "\`$$($Name)\:" + if (!$found) { + return $ScriptBlock + } + + # loop and replace "set" syntax + while ($strScriptBlock -imatch $scopedVar.Set.Pattern) { + $setReplace = $scopedVar.Set.Replace.Replace('{{name}}', $Matches['name']) + $strScriptBlock = $strScriptBlock.Replace($Matches['full'], $setReplace) + } + + # loop and replace "get" syntax + while ($strScriptBlock -imatch $scopedVar.Get.Pattern) { + $getReplace = $scopedVar.Get.Replace.Replace('{{name}}', $Matches['name']) + $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))") + } + + # convert update scriptblock back + return [scriptblock]::Create($strScriptBlock) + } +} + +function Add-PodeScopedVariable { + [CmdletBinding(DefaultParameterSetName = 'Replace')] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Replace')] + [string] + $SetReplace, + + [Parameter(Mandatory = $true, ParameterSetName = 'Replace')] + [string] + $GetReplace, + + [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')] + [scriptblock] + $ScriptBlock + ) + + # check if var already defined + if (Test-PodeScopedVariable -Name $Name) { + throw "Scoped Variable already defined: $($Name)" + } + + # add scoped var definition + $PodeContext.Server.ScopedVariables[$Name] = @{ + $Name = $Name + ScriptBlock = $ScriptBlock + Set = @{ + Pattern = "(?\`$$($Name)\:(?[a-z0-9_\?]+)\s*=)" + Replace = $SetReplace + } + Get = @{ + Pattern = "(?\`$$($Name)\:(?[a-z0-9_\?]+))" + Replace = $GetReplace + } + } +} + +function Remove-PodeScopedVariable { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + $null = $PodeContext.Server.ScopedVariables.Remove($Name) +} + +function Test-PodeScopedVariable { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.ScopedVariables.Contains($Name) +} + +function Clear-PodeScopedVariables { + $null = $PodeContext.Server.ScopedVariables.Clear() +} + +function Get-PodeScopedVariable { + [CmdletBinding()] + param( + [Parameter()] + [string[]] + $Name + ) + + # return all if no Name + if ([string]::IsNullOrEmpty($Name) -or ($Name.Length -eq 0)) { + return $PodeContext.Server.ScopedVariables.Values + } + + # return filtered + return @(foreach ($n in $Name) { + $PodeContext.Server.ScopedVariables[$n] + }) +} + +function Use-PodeScopedVariables { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Path + ) + + Use-PodeFolder -Path $Path -DefaultPath 'scoped-vars' +} \ No newline at end of file diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index f9da5c770..8ef925fad 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -183,6 +183,7 @@ Describe 'Restart-PodeInternalServer' { Items = @{} Storage = @{} } + ScopedVariables = @{} } Metrics = @{ Server = @{ From 638a714aa5a4f4e97df7f77b48fd99024b576aea Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Fri, 22 Dec 2023 13:17:10 +0000 Subject: [PATCH 08/84] #1207: fix tests --- tests/unit/Server.Tests.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 8ef925fad..078d153ad 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -29,6 +29,7 @@ Describe 'Start-PodeInternalServer' { Mock Start-PodeCacheHousekeeper { } Mock Invoke-PodeEvent { } Mock Write-Verbose { } + Mock Add-PodeScopedVariablesInbuilt { } It 'Calls one-off script logic' { $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {} } From ac985dba61fd57547d28cc79b3cf880673b2a404 Mon Sep 17 00:00:00 2001 From: Szeraax <6242511+Szeraax@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:31:05 -0700 Subject: [PATCH 09/84] Update Sessions.md --- docs/Tutorials/Middleware/Types/Sessions.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/Tutorials/Middleware/Types/Sessions.md b/docs/Tutorials/Middleware/Types/Sessions.md index fc1c04e5d..858b19de2 100644 --- a/docs/Tutorials/Middleware/Types/Sessions.md +++ b/docs/Tutorials/Middleware/Types/Sessions.md @@ -66,7 +66,7 @@ You can define a custom storage by supplying a `psobject` to the `-Storage` para [void] Delete([string] $sessionId) ``` -For example, the following is a mock up of a Storage for Redis (note that the functions are fake): +For example, the following is a mock up of a Storage for Redis. Note that the functions are fake and also that the returned User property in the hashtable MUST be an object (such as via PSCO cast): ```powershell # create the object @@ -76,7 +76,12 @@ $store = New-Object -TypeName psobject $store | Add-Member -MemberType NoteProperty -Name Get -Value { param($sessionId) $data = Get-RedisKey -Key $sessionId - return ($data | ConvertFrom-Json -AsHashtable) + $session = $data | ConvertFrom-Json -AsHashtable + try { + $session.Data.Auth.User = [PSCustomObject]$session.Data.Auth.User + } + catch {} + return $session } # add a Set property to save a session's data From 6bf488690d3431f9fbc7749f46f0a3035925d9ad Mon Sep 17 00:00:00 2001 From: Szeraax <6242511+Szeraax@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:59:55 -0700 Subject: [PATCH 10/84] Update Tasks.md Edit ArgumentList --- docs/Tutorials/Tasks.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/Tutorials/Tasks.md b/docs/Tutorials/Tasks.md index b7d44a02b..2cc43beac 100644 --- a/docs/Tutorials/Tasks.md +++ b/docs/Tutorials/Tasks.md @@ -43,7 +43,7 @@ Start-PodeServer -EnablePool Tasks { You can supply custom arguments to your tasks by using the `-ArgumentList` parameter. Similar to schedules, for tasks the `-ArgumentList` is a hashtable; this is done because parameters to the `-ScriptBlock` are splatted in, and the parameter names are literal. -For example, the first parameter to a task is always `$Event` - this contains the `.Lockable` object. Other parameters come from any Key/Values contained with the optional `-ArgumentList`: +There is always a parameter added to each task invocation in the `-Event` argument - this contains the `.Lockable` object. You can safely ignore/leave the parameter unbound if you do not need it. Other parameters come from any Key/Values contained with the optional `-ArgumentList`: ```powershell Add-PodeTask -Name 'Example' -ArgumentList @{ Name = 'Rick'; Environment = 'Multiverse' } -ScriptBlock { @@ -51,6 +51,18 @@ Add-PodeTask -Name 'Example' -ArgumentList @{ Name = 'Rick'; Environment = 'Mult } ``` +Tasks parameters **must** be bound in the param block in order to be used, but the values for the paramters can be set through the `-ArgumentList` hashtable parameter in either the Add-PodeTask definition or when invoking the task. The following snippet would populate the parameters to the task with the same values as the above example but the `-ArgumentList` parameter is populated during invocation. Note that Keys in the `-ArgumentList` hashtable parameter set during invocation override the same Keys set during task creation: + +```powershell +Add-PodeTask -Name 'Example' -ScriptBlock { + param($Event, $Name, $Environment) +} + +Add-PodeRoute -Method Get -Path '/invoke-task' -ScriptBlock { + Invoke-PodeTask -Name 'example' -ArgumentList @{ Name = 'Rick'; Environment = 'Multiverse' } +} +``` + !!! important In tasks, your scriptblock parameter names must be exact - including case-sensitivity. This is because the arguments are splatted into a runspace. If you pass in an argument called "Names", the param-block must have `$Names` exactly. Furthermore, the event parameter *must* be called `$Event`. From fa858410f6654790feec52e054722e3fce08a80f Mon Sep 17 00:00:00 2001 From: Szeraax <6242511+Szeraax@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:01:51 -0700 Subject: [PATCH 11/84] Update Tasks.md --- docs/Tutorials/Tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tutorials/Tasks.md b/docs/Tutorials/Tasks.md index 2cc43beac..bc338babb 100644 --- a/docs/Tutorials/Tasks.md +++ b/docs/Tutorials/Tasks.md @@ -2,7 +2,7 @@ A Task in Pode is a script that you can later invoke either asynchronously, or synchronously. They can be invoked many times, and they also support returning values from them for later use. -Similar to [Schedules](../Schedules), Tasks also run in their own separate runspaces; meaning you can have long or short running tasks. By default up to a maximum of 2 tasks can run concurrently, but this can be changed by using [`Set-PodeTaskConcurrency`](../../Functions/Tasks/Set-PodeTaskConcurrency). +Similar to [Schedules](../Schedules), Tasks also run in their own separate runspaces; meaning you can have long or short running tasks. By default up to a maximum of 2 tasks can run concurrently, but this can be changed by using [`Set-PodeTaskConcurrency`](../../Functions/Tasks/Set-PodeTaskConcurrency). When more tasks are invoke than can be run concurrently, tasks will be added to the task queue and will run once there is available resource in the thread pool. Behind the scenes there is a a Timer created that will automatically clean-up any completed tasks. Any task that has been completed for 1+ minutes will be disposed of to free up resources - there are functions which will let you clean-up tasks more quickly. From 63be5d7ab7e8f4985b501649dbd733446efd05d1 Mon Sep 17 00:00:00 2001 From: Szeraax <6242511+Szeraax@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:02:34 -0700 Subject: [PATCH 12/84] Update Tasks.md --- docs/Tutorials/Tasks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tutorials/Tasks.md b/docs/Tutorials/Tasks.md index bc338babb..dc7cbf47c 100644 --- a/docs/Tutorials/Tasks.md +++ b/docs/Tutorials/Tasks.md @@ -2,7 +2,7 @@ A Task in Pode is a script that you can later invoke either asynchronously, or synchronously. They can be invoked many times, and they also support returning values from them for later use. -Similar to [Schedules](../Schedules), Tasks also run in their own separate runspaces; meaning you can have long or short running tasks. By default up to a maximum of 2 tasks can run concurrently, but this can be changed by using [`Set-PodeTaskConcurrency`](../../Functions/Tasks/Set-PodeTaskConcurrency). When more tasks are invoke than can be run concurrently, tasks will be added to the task queue and will run once there is available resource in the thread pool. +Similar to [Schedules](../Schedules), Tasks also run in their own separate runspaces; meaning you can have long or short running tasks. By default up to a maximum of 2 tasks can run concurrently, but this can be changed by using [`Set-PodeTaskConcurrency`](../../Functions/Tasks/Set-PodeTaskConcurrency). When more tasks are invoked than can be run concurrently, tasks will be added to the task queue and will run once there is available resource in the thread pool. Behind the scenes there is a a Timer created that will automatically clean-up any completed tasks. Any task that has been completed for 1+ minutes will be disposed of to free up resources - there are functions which will let you clean-up tasks more quickly. From cbe52f7558eca6377fc92b9c23dd9e09d6b6bf46 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 30 Jan 2024 21:25:35 +0000 Subject: [PATCH 13/84] #1228: fix ordering of static content routes --- src/Private/Routes.ps1 | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/Private/Routes.ps1 b/src/Private/Routes.ps1 index 427b115d2..0f8c7f09d 100644 --- a/src/Private/Routes.ps1 +++ b/src/Private/Routes.ps1 @@ -50,23 +50,31 @@ function Find-PodeRoute { } } - # is this a static route? - $isStatic = ($Method -ieq 'static') - # first ensure we have the method $_method = $PodeContext.Server.Routes[$Method] if ($null -eq $_method) { return $null } + # is this a static route? + $isStatic = ($Method -ieq 'static') + # if we have a perfect match for the route, return it if the protocol is right - $found = Get-PodeRouteByUrl -Routes $_method[$Path] -EndpointName $EndpointName - if (!$isStatic -and ($null -ne $found)) { - return $found + if (!$isStatic) { + $found = Get-PodeRouteByUrl -Routes $_method[$Path] -EndpointName $EndpointName + if ($null -ne $found) { + return $found + } } # otherwise, match the path to routes on regex (first match only) - $valid = @(foreach ($key in $_method.Keys) { + $paths = @($_method.Keys) + if ($isStatic) { + [array]::Sort($paths) + [array]::Reverse($paths) + } + + $valid = @(foreach ($key in $paths) { if ($Path -imatch "^$($key)$") { $key break From 9a57c272ad433fc9d86a760312dda0ac3bc64332 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Fri, 2 Feb 2024 22:19:45 +0000 Subject: [PATCH 14/84] #1228: adds a -RedirectToDefault switch to static routes --- src/Private/Context.ps1 | 5 +- src/Private/PodeServer.ps1 | 4 ++ src/Private/Routes.ps1 | 20 ++++++-- src/Private/Serverless.ps1 | 8 ++++ src/Public/Routes.ps1 | 93 +++++++++++++++++++++++++------------- 5 files changed, 93 insertions(+), 37 deletions(-) diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 73e855bf6..dae736630 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -816,8 +816,9 @@ function Set-PodeWebConfiguration { # setup the main web config $Context.Server.Web = @{ Static = @{ - Defaults = $Configuration.Static.Defaults - Cache = @{ + Defaults = $Configuration.Static.Defaults + RedirectToDefault = [bool]$Configuration.Static.RedirectToDefault + Cache = @{ Enabled = [bool]$Configuration.Static.Cache.Enable MaxAge = [int](Protect-PodeValue -Value $Configuration.Static.Cache.MaxAge -Default 3600) Include = (Convert-PodePathPatternsToRegex -Paths @($Configuration.Static.Cache.Include) -NotSlashes) diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index ad10d44cd..557fda99d 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -189,6 +189,10 @@ function Start-PodeWebServer { if ($WebEvent.StaticContent.IsDownload) { Set-PodeResponseAttachment -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name } + elseif ($WebEvent.StaticContent.RedirectToDefault) { + $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) + Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)" + } else { $cachable = $WebEvent.StaticContent.IsCachable Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable diff --git a/src/Private/Routes.ps1 b/src/Private/Routes.ps1 index 0f8c7f09d..59131964a 100644 --- a/src/Private/Routes.ps1 +++ b/src/Private/Routes.ps1 @@ -139,6 +139,8 @@ function Find-PodeStaticRoute { $found = Find-PodeRoute -Method 'static' -Path $Path -EndpointName $EndpointName $download = ([bool]$found.Download) $source = $null + $isDefault = $false + $redirectToDefault = ([bool]$found.RedirectToDefault) # if we have a defined static route, use that if ($null -ne $found) { @@ -152,11 +154,13 @@ function Find-PodeStaticRoute { if (!$found.Download -and !(Test-PodePathIsFile $file) -and (Get-PodeCount @($found.Defaults)) -gt 0) { if ((Get-PodeCount @($found.Defaults)) -eq 1) { $file = [System.IO.Path]::Combine($file, @($found.Defaults)[0]) + $isDefault = $true } else { foreach ($def in $found.Defaults) { if (Test-PodePath ([System.IO.Path]::Combine($found.Source, $def)) -NoStatus) { $file = [System.IO.Path]::Combine($file, $def) + $isDefault = $true break } } @@ -171,6 +175,8 @@ function Find-PodeStaticRoute { $source = Find-PodePublicRoute -Path $Path $download = $false $found = $null + $isDefault = $false + $redirectToDefault = $false } # return nothing if no source @@ -179,11 +185,19 @@ function Find-PodeStaticRoute { } # return the route details + if ($redirectToDefault -and $isDefault) { + $redirectToDefault = $true + } + else { + $redirectToDefault = $false + } + return @{ Content = @{ - Source = $source - IsDownload = $download - IsCachable = (Test-PodeRouteValidForCaching -Path $Path) + Source = $source + IsDownload = $download + IsCachable = (Test-PodeRouteValidForCaching -Path $Path) + RedirectToDefault = $redirectToDefault } Route = $found } diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index a7aac81ca..d861501e1 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -85,6 +85,10 @@ function Start-PodeAzFuncServer { if ($WebEvent.StaticContent.IsDownload) { Set-PodeResponseAttachment -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name } + elseif ($WebEvent.StaticContent.RedirectToDefault) { + $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) + Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)" + } else { $cachable = $WebEvent.StaticContent.IsCachable Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable @@ -194,6 +198,10 @@ function Start-PodeAwsLambdaServer { if ($WebEvent.StaticContent.IsDownload) { Set-PodeResponseAttachment -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name } + elseif ($WebEvent.StaticContent.RedirectToDefault) { + $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) + Move-PodeResponseUrl -Url "$($WebEvent.Path)/$($file)" + } else { $cachable = $WebEvent.StaticContent.IsCachable Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index ae6d1d1e5..f1ab7bf84 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -476,6 +476,9 @@ One or more optional Scopes that will be authorised to access this Route, when u .PARAMETER User One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER RedirectToDefault +If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path. + .EXAMPLE Add-PodeStaticRoute -Path '/assets' -Source './assets' @@ -484,6 +487,9 @@ Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') .EXAMPLE Add-PodeStaticRoute -Path '/installers' -Source './exes' -DownloadOnly + +.EXAMPLE +Add-PodeStaticRoute -Path '/assets' -Source './assets' -Defaults @('index.html') -RedirectToDefault #> function Add-PodeStaticRoute { [CmdletBinding()] @@ -558,7 +564,10 @@ function Add-PodeStaticRoute { $DownloadOnly, [switch] - $PassThru + $PassThru, + + [switch] + $RedirectToDefault ) # check if we have any route group info defined @@ -611,6 +620,10 @@ function Add-PodeStaticRoute { $DownloadOnly = $RouteGroup.DownloadOnly } + if ($RouteGroup.RedirectToDefault) { + $RedirectToDefault = $RouteGroup.RedirectToDefault + } + if ($RouteGroup.IfExists -ine 'default') { $IfExists = $RouteGroup.IfExists } @@ -700,6 +713,10 @@ function Add-PodeStaticRoute { $Defaults = Get-PodeStaticRouteDefaults } + if (!$RedirectToDefault) { + $RedirectToDefault = $PodeContext.Server.Web.Static.RedirectToDefault + } + # convert any middleware into valid hashtables $Middleware = @(ConvertTo-PodeMiddleware -Middleware $Middleware -PSSession $PSCmdlet.SessionState) @@ -744,30 +761,31 @@ function Add-PodeStaticRoute { Write-Verbose "Adding Route: [$($Method)] $($Path)" $newRoutes = @(foreach ($_endpoint in $endpoints) { @{ - Source = $Source - Path = $Path - Method = $Method - Defaults = $Defaults - Middleware = $Middleware - Authentication = $Authentication - Access = $Access - AccessMeta = @{ + Source = $Source + Path = $Path + Method = $Method + Defaults = $Defaults + RedirectToDefault = $RedirectToDefault + Middleware = $Middleware + Authentication = $Authentication + Access = $Access + AccessMeta = @{ Role = $Role Group = $Group Scope = $Scope User = $User Custom = $CustomAccess } - Endpoint = @{ + Endpoint = @{ Protocol = $_endpoint.Protocol Address = $_endpoint.Address.Trim() Name = $_endpoint.Name } - ContentType = $ContentType - TransferEncoding = $TransferEncoding - ErrorType = $ErrorContentType - Download = $DownloadOnly - OpenApi = @{ + ContentType = $ContentType + TransferEncoding = $TransferEncoding + ErrorType = $ErrorContentType + Download = $DownloadOnly + OpenApi = @{ Path = $OpenApiPath Responses = @{ '200' = @{ description = 'OK' } @@ -777,8 +795,8 @@ function Add-PodeStaticRoute { RequestBody = @{} Authentication = @() } - IsStatic = $true - Metrics = @{ + IsStatic = $true + Metrics = @{ Requests = @{ Total = 0 StatusCodes = @{} @@ -1235,6 +1253,9 @@ One or more optional Scopes that will be authorised to access this Route, when u .PARAMETER User One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER RedirectToDefault +If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path. + .EXAMPLE Add-PodeStaticRouteGroup -Path '/static' -Routes { Add-PodeStaticRoute -Path '/images' -Etc } #> @@ -1312,7 +1333,10 @@ function Add-PodeStaticRouteGroup { $AllowAnon, [switch] - $DownloadOnly + $DownloadOnly, + + [switch] + $RedirectToDefault ) if (Test-PodeIsEmpty $Routes) { @@ -1376,6 +1400,10 @@ function Add-PodeStaticRouteGroup { $DownloadOnly = $RouteGroup.DownloadOnly } + if ($RouteGroup.RedirectToDefault) { + $RedirectToDefault = $RouteGroup.RedirectToDefault + } + if ($RouteGroup.IfExists -ine 'default') { $IfExists = $RouteGroup.IfExists } @@ -1402,20 +1430,21 @@ function Add-PodeStaticRouteGroup { } $RouteGroup = @{ - Path = $Path - Source = $Source - Middleware = $Middleware - EndpointName = $EndpointName - ContentType = $ContentType - TransferEncoding = $TransferEncoding - Defaults = $Defaults - ErrorContentType = $ErrorContentType - Authentication = $Authentication - Access = $Access - AllowAnon = $AllowAnon - DownloadOnly = $DownloadOnly - IfExists = $IfExists - AccessMeta = @{ + Path = $Path + Source = $Source + Middleware = $Middleware + EndpointName = $EndpointName + ContentType = $ContentType + TransferEncoding = $TransferEncoding + Defaults = $Defaults + RedirectToDefault = $RedirectToDefault + ErrorContentType = $ErrorContentType + Authentication = $Authentication + Access = $Access + AllowAnon = $AllowAnon + DownloadOnly = $DownloadOnly + IfExists = $IfExists + AccessMeta = @{ Role = $Role Group = $Group Scope = $Scope From 692dc793b963c7564af6ccad5880bf1b840aa9ae Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 6 Feb 2024 23:00:35 +0000 Subject: [PATCH 15/84] #1207: make scriptblock merging function public, and return early for using vars if there are none --- src/Pode.psd1 | 1 + src/Private/Authentication.ps1 | 10 +++++----- src/Private/Endware.ps1 | 2 +- src/Private/Events.ps1 | 2 +- src/Private/FileWatchers.ps1 | 2 +- src/Private/Helpers.ps1 | 31 ++----------------------------- src/Private/Logging.ps1 | 6 +++--- src/Private/Middleware.ps1 | 2 +- src/Private/PodeServer.ps1 | 4 ++-- src/Private/ScopedVariables.ps1 | 6 ++++++ src/Private/Serverless.ps1 | 4 ++-- src/Private/ServiceServer.ps1 | 2 +- src/Private/SmtpServer.ps1 | 2 +- src/Private/TcpServer.ps1 | 2 +- src/Private/Timers.ps1 | 2 +- src/Private/WebSockets.ps1 | 2 +- src/Public/Access.ps1 | 6 +++--- src/Public/Routes.ps1 | 6 +++--- src/Public/Utilities.ps1 | 27 +++++++++++++++++++++++++++ 19 files changed, 63 insertions(+), 56 deletions(-) diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 406158e30..c73165e3d 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -103,6 +103,7 @@ 'Protect-PodeValue', 'Resolve-PodeValue', 'Invoke-PodeScriptBlock', + 'Merge-PodeScriptblockArguments', 'Test-PodeIsUnix', 'Test-PodeIsWindows', 'Test-PodeIsMacOS', diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index b681d5b0a..86bccd08a 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -831,7 +831,7 @@ function Invoke-PodeAuthInbuiltScriptBlock { $UsingVariables ) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $User -UsingVariables $UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $User -UsingVariables $UsingVariables) return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $_args -Return -Splat) } @@ -1167,7 +1167,7 @@ function Test-PodeAuthValidation { } # run auth scheme script to parse request for data - $_args = @(Get-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $auth.Scheme.Arguments -UsingVariables $auth.Scheme.ScriptBlock.UsingVariables) # call inner schemes first if ($null -ne $auth.Scheme.InnerScheme) { @@ -1180,7 +1180,7 @@ function Test-PodeAuthValidation { }) for ($i = $_inner.Length - 1; $i -ge 0; $i--) { - $_tmp_args = @(Get-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables) + $_tmp_args = @(Merge-PodeScriptblockArguments -ArgumentList $_inner[$i].Arguments -UsingVariables $_inner[$i].ScriptBlock.UsingVariables) $_tmp_args += , $schemes $result = (Invoke-PodeScriptBlock -ScriptBlock $_inner[$i].ScriptBlock.Script -Arguments $_tmp_args -Return -Splat) @@ -1204,13 +1204,13 @@ function Test-PodeAuthValidation { $original = $result $_args = @($result) + @($auth.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $auth.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $auth.UsingVariables) $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -Return -Splat) # if we have user, then run post validator if present if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) { $_args = @($original) + @($result) + @($auth.Scheme.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables) $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -Return -Splat) } } diff --git a/src/Private/Endware.ps1 b/src/Private/Endware.ps1 index 508842b0c..740baa10f 100644 --- a/src/Private/Endware.ps1 +++ b/src/Private/Endware.ps1 @@ -16,7 +16,7 @@ function Invoke-PodeEndware { } try { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $eware.Arguments -UsingVariables $eware.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $eware.Arguments -UsingVariables $eware.UsingVariables) $null = Invoke-PodeScriptBlock -ScriptBlock $eware.Logic -Arguments $_args -Scoped -Splat } catch { diff --git a/src/Private/Events.ps1 b/src/Private/Events.ps1 index 2025bb508..27828cebe 100644 --- a/src/Private/Events.ps1 +++ b/src/Private/Events.ps1 @@ -18,7 +18,7 @@ function Invoke-PodeEvent { } try { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $evt.Arguments -UsingVariables $evt.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $evt.Arguments -UsingVariables $evt.UsingVariables) $null = Invoke-PodeScriptBlock -ScriptBlock $evt.ScriptBlock -Arguments $_args -Scoped -Splat } catch { diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index 7446ddb90..a5546ba55 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -99,7 +99,7 @@ function Start-PodeFileWatcherRunspace { } # invoke main script - $_args = @(Get-PodeScriptblockArguments -ArgumentList $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $_args -Scoped -Splat } catch [System.OperationCanceledException] { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 42abd59e1..4aa8d0bcc 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -80,7 +80,7 @@ function Get-PodeFileContentUsingViewEngine { $_args = @($Path, $Data) } - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables) $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.ScriptBlock -Arguments $_args -Return -Splat) } } @@ -1484,7 +1484,7 @@ function ConvertFrom-PodeRequestContent { # check if there is a defined custom body parser if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { $parser = $PodeContext.Server.BodyParsers[$ContentType] - $_args = @(Get-PodeScriptblockArguments -ArgumentList $Content -UsingVariables $parser.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $Content -UsingVariables $parser.UsingVariables) $Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $parser.ScriptBlock -Arguments $_args -Return) $Content = $null return $Result @@ -2683,33 +2683,6 @@ function Find-PodeModuleFile { return $path } -function Get-PodeScriptblockArguments { - param( - [Parameter()] - [object[]] - $ArgumentList, - - [Parameter()] - [object[]] - $UsingVariables - ) - - if ($null -eq $ArgumentList) { - $ArgumentList = @() - } - - if (($null -eq $UsingVariables) -or ($UsingVariables.Length -le 0)) { - return $ArgumentList - } - - $_vars = @() - foreach ($_var in $UsingVariables) { - $_vars += , $_var.Value - } - - return ($_vars + $ArgumentList) -} - function Clear-PodeHashtableInnerKeys { param( [Parameter(ValueFromPipeline = $true)] diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index 24f8f393f..ecfe51845 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -367,7 +367,7 @@ function Start-PodeLoggingRunspace { # convert to log item into a writable format $_args = @($log.Item) + @($logger.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.UsingVariables) $rawItems = $log.Item $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -Return -Splat) @@ -397,7 +397,7 @@ function Start-PodeLoggingRunspace { # send the writable log item off to the log writer if ($null -ne $result) { $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.Method.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.Method.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -Splat } @@ -423,7 +423,7 @@ function Test-PodeLoggerBatches { $batch.RawItems = @() $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.Method.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.Method.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -Splat } } diff --git a/src/Private/Middleware.ps1 b/src/Private/Middleware.ps1 index a0cb93b6b..42753964c 100644 --- a/src/Private/Middleware.ps1 +++ b/src/Private/Middleware.ps1 @@ -38,7 +38,7 @@ function Invoke-PodeMiddleware { } try { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $midware.Arguments -UsingVariables $midware.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $midware.Arguments -UsingVariables $midware.UsingVariables) $continue = Invoke-PodeScriptBlock -ScriptBlock $midware.Logic -Arguments $_args -Return -Scoped -Splat if ($null -eq $continue) { $continue = $true diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index ad10d44cd..8b274bccc 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -195,7 +195,7 @@ function Start-PodeWebServer { } } elseif ($null -ne $WebEvent.Route.Logic) { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $_args -Scoped -Splat } } @@ -369,7 +369,7 @@ function Start-PodeWebServer { $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name if ($null -ne $SignalEvent.Route) { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $_args -Scoped -Splat } else { diff --git a/src/Private/ScopedVariables.ps1 b/src/Private/ScopedVariables.ps1 index 0632b246a..d13950111 100644 --- a/src/Private/ScopedVariables.ps1 +++ b/src/Private/ScopedVariables.ps1 @@ -48,6 +48,12 @@ function Add-PodeScopedVariableInbuiltUsing { $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "`$using:$($Matches['name'])") } + # just return if there are no $using: + if ($strScriptBlock -inotmatch '\$using:') { + return $ScriptBlock, $null + } + + # if we found any inner usings, recreate the scriptblock if ($foundInnerUsing) { $ScriptBlock = [scriptblock]::Create($strScriptBlock) } diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index a7aac81ca..bbc1b1288 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -91,7 +91,7 @@ function Start-PodeAzFuncServer { } } else { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $_args -Scoped -Splat } } @@ -200,7 +200,7 @@ function Start-PodeAwsLambdaServer { } } else { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $_args -Scoped -Splat } } diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index 736f610fa..0199a0faa 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -20,7 +20,7 @@ function Start-PodeServiceServer { $handlers = Get-PodeHandler -Type Service foreach ($name in $handlers.Keys) { $handler = $handlers[$name] - $_args = @(Get-PodeScriptblockArguments -ArgumentList $handler.Arguments -UsingVariables $handler.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $handler.Arguments -UsingVariables $handler.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $_args -Scoped -Splat } diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index 66ddf908c..050fcdc88 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -140,7 +140,7 @@ function Start-PodeSmtpServer { $handlers = Get-PodeHandler -Type Smtp foreach ($name in $handlers.Keys) { $handler = $handlers[$name] - $_args = @(Get-PodeScriptblockArguments -ArgumentList $handler.Arguments -UsingVariables $handler.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $handler.Arguments -UsingVariables $handler.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $_args -Scoped -Splat } } diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 640e6ca26..9b38df87c 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -149,7 +149,7 @@ function Start-PodeTcpServer { # invoke it if ($null -ne $verb.Logic) { - $_args = @(Get-PodeScriptblockArguments -ArgumentList $verb.Arguments -UsingVariables $verb.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $verb.Arguments -UsingVariables $verb.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $_args -Scoped -Splat } diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index 191d49685..95e1d5b2f 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -89,7 +89,7 @@ function Invoke-PodeInternalTimer { } # add timer $using args - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $Timer.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $Timer.UsingVariables) # invoke timer Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -Scoped -Splat diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index df3beff39..96a103164 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -78,7 +78,7 @@ function Start-PodeWebSocketRunspace { $WsEvent.Files = $result.Files # invoke websocket script - $_args = @(Get-PodeScriptblockArguments -ArgumentList $websocket.Arguments -UsingVariables $websocket.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $websocket.Arguments -UsingVariables $websocket.UsingVariables) Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $_args -Scoped -Splat } catch [System.OperationCanceledException] {} diff --git a/src/Public/Access.ps1 b/src/Public/Access.ps1 index 90bed1981..fa02559b3 100644 --- a/src/Public/Access.ps1 +++ b/src/Public/Access.ps1 @@ -429,7 +429,7 @@ function Test-PodeAccess { if (($null -eq $Source) -or ($Source.Length -eq 0)) { if ($null -ne $access.Scheme.ScriptBlock) { $_args = $ArgumentList + @($access.Scheme.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -Return -Splat } } @@ -437,7 +437,7 @@ function Test-PodeAccess { # check for custom validator, or use default match logic if ($null -ne $access.ScriptBlock) { $_args = @(, $Source) + @(, $Destination) + @($access.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.ScriptBlock.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.ScriptBlock.UsingVariables) return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -Return -Splat) } @@ -528,7 +528,7 @@ function Test-PodeAccessUser { # otherwise, invoke scriptblock else { $_args = @($user) + @($access.Scheme.Arguments) - $_args = @(Get-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) + $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -Return -Splat } diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index ae6d1d1e5..5c7521d60 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -1170,7 +1170,7 @@ function Add-PodeRouteGroup { } # add routes - $_args = @(Get-PodeScriptblockArguments -UsingVariables $usingVars) + $_args = @(Merge-PodeScriptblockArguments -UsingVariables $usingVars) $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -Arguments $_args -Splat } @@ -1425,7 +1425,7 @@ function Add-PodeStaticRouteGroup { } # add routes - $_args = @(Get-PodeScriptblockArguments -UsingVariables $usingVars) + $_args = @(Merge-PodeScriptblockArguments -UsingVariables $usingVars) $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -Arguments $_args -Splat } @@ -1506,7 +1506,7 @@ function Add-PodeSignalRouteGroup { } # add routes - $_args = @(Get-PodeScriptblockArguments -UsingVariables $usingVars) + $_args = @(Merge-PodeScriptblockArguments -UsingVariables $usingVars) $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -Arguments $_args -Splat } diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 8074c3379..0fc6d2995 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -547,6 +547,33 @@ function Invoke-PodeScriptBlock { } } +function Merge-PodeScriptblockArguments { + param( + [Parameter()] + [object[]] + $ArgumentList, + + [Parameter()] + [object[]] + $UsingVariables + ) + + if ($null -eq $ArgumentList) { + $ArgumentList = @() + } + + if (($null -eq $UsingVariables) -or ($UsingVariables.Length -le 0)) { + return $ArgumentList + } + + $_vars = @() + foreach ($_var in $UsingVariables) { + $_vars += , $_var.Value + } + + return ($_vars + $ArgumentList) +} + <# .SYNOPSIS Tests if a value is empty - the value can be of any type. From 633ab57597d0662b3808e3dcfc96c4af4984cee1 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sat, 10 Feb 2024 17:29:57 +0000 Subject: [PATCH 16/84] #1207: add function summaries, and simplify invoking of scriptblocks/merging args --- src/Private/Authentication.ps1 | 9 +- src/Private/Endware.ps1 | 3 +- src/Private/Events.ps1 | 3 +- src/Private/FileWatchers.ps1 | 3 +- src/Private/Helpers.ps1 | 6 +- src/Private/Logging.ps1 | 12 +-- src/Private/Middleware.ps1 | 3 +- src/Private/PodeServer.ps1 | 6 +- src/Private/Serverless.ps1 | 6 +- src/Private/ServiceServer.ps1 | 3 +- src/Private/SmtpServer.ps1 | 3 +- src/Private/TcpServer.ps1 | 3 +- src/Private/Timers.ps1 | 5 +- src/Private/WebSockets.ps1 | 3 +- src/Public/Access.ps1 | 9 +- src/Public/Routes.ps1 | 9 +- src/Public/ScopedVariables.ps1 | 178 +++++++++++++++++++++++++++++++-- src/Public/Utilities.ps1 | 40 +++++++- 18 files changed, 237 insertions(+), 67 deletions(-) diff --git a/src/Private/Authentication.ps1 b/src/Private/Authentication.ps1 index 86bccd08a..041d72d4e 100644 --- a/src/Private/Authentication.ps1 +++ b/src/Private/Authentication.ps1 @@ -831,8 +831,7 @@ function Invoke-PodeAuthInbuiltScriptBlock { $UsingVariables ) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $User -UsingVariables $UsingVariables) - return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $_args -Return -Splat) + return (Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Arguments $User -UsingVariables $UsingVariables -Return -Splat) } function Get-PodeAuthWindowsLocalMethod { @@ -1204,14 +1203,12 @@ function Test-PodeAuthValidation { $original = $result $_args = @($result) + @($auth.Arguments) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $auth.UsingVariables) - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -Return -Splat) + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.ScriptBlock -Arguments $_args -UsingVariables $auth.UsingVariables -Return -Splat) # if we have user, then run post validator if present if ([string]::IsNullOrEmpty($result.Code) -and ($null -ne $auth.Scheme.PostValidator.Script)) { $_args = @($original) + @($result) + @($auth.Scheme.Arguments) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables) - $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -Return -Splat) + $result = (Invoke-PodeScriptBlock -ScriptBlock $auth.Scheme.PostValidator.Script -Arguments $_args -UsingVariables $auth.Scheme.PostValidator.UsingVariables -Return -Splat) } } diff --git a/src/Private/Endware.ps1 b/src/Private/Endware.ps1 index 740baa10f..65c4e5cee 100644 --- a/src/Private/Endware.ps1 +++ b/src/Private/Endware.ps1 @@ -16,8 +16,7 @@ function Invoke-PodeEndware { } try { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $eware.Arguments -UsingVariables $eware.UsingVariables) - $null = Invoke-PodeScriptBlock -ScriptBlock $eware.Logic -Arguments $_args -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $eware.Logic -Arguments $eware.Arguments -UsingVariables $eware.UsingVariables -Scoped -Splat } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/Events.ps1 b/src/Private/Events.ps1 index 27828cebe..592f51817 100644 --- a/src/Private/Events.ps1 +++ b/src/Private/Events.ps1 @@ -18,8 +18,7 @@ function Invoke-PodeEvent { } try { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $evt.Arguments -UsingVariables $evt.UsingVariables) - $null = Invoke-PodeScriptBlock -ScriptBlock $evt.ScriptBlock -Arguments $_args -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $evt.ScriptBlock -Arguments $evt.Arguments -UsingVariables $evt.UsingVariables -Scoped -Splat } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index a5546ba55..2bc7e6788 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -99,8 +99,7 @@ function Start-PodeFileWatcherRunspace { } # invoke main script - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat } catch [System.OperationCanceledException] { } diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 4aa8d0bcc..ecc38dc86 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -80,8 +80,7 @@ function Get-PodeFileContentUsingViewEngine { $_args = @($Path, $Data) } - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables) - $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.ScriptBlock -Arguments $_args -Return -Splat) + $content = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.ViewEngine.ScriptBlock -Arguments $_args -UsingVariables $PodeContext.Server.ViewEngine.UsingVariables -Return -Splat) } } } @@ -1484,8 +1483,7 @@ function ConvertFrom-PodeRequestContent { # check if there is a defined custom body parser if ($PodeContext.Server.BodyParsers.ContainsKey($ContentType)) { $parser = $PodeContext.Server.BodyParsers[$ContentType] - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $Content -UsingVariables $parser.UsingVariables) - $Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $parser.ScriptBlock -Arguments $_args -Return) + $Result.Data = (Invoke-PodeScriptBlock -ScriptBlock $parser.ScriptBlock -Arguments $Content -UsingVariables $parser.UsingVariables -Return) $Content = $null return $Result } diff --git a/src/Private/Logging.ps1 b/src/Private/Logging.ps1 index ecfe51845..ad67d42ad 100644 --- a/src/Private/Logging.ps1 +++ b/src/Private/Logging.ps1 @@ -366,11 +366,9 @@ function Start-PodeLoggingRunspace { } # convert to log item into a writable format - $_args = @($log.Item) + @($logger.Arguments) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.UsingVariables) - $rawItems = $log.Item - $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -Return -Splat) + $_args = @($log.Item) + @($logger.Arguments) + $result = @(Invoke-PodeScriptBlock -ScriptBlock $logger.ScriptBlock -Arguments $_args -UsingVariables $logger.UsingVariables -Return -Splat) # check batching $batch = $logger.Method.Batch @@ -397,8 +395,7 @@ function Start-PodeLoggingRunspace { # send the writable log item off to the log writer if ($null -ne $result) { $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.Method.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat } # small sleep to lower cpu usage @@ -423,8 +420,7 @@ function Test-PodeLoggerBatches { $batch.RawItems = @() $_args = @(, $result) + @($logger.Method.Arguments) + @(, $rawItems) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $logger.Method.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $logger.Method.ScriptBlock -Arguments $_args -UsingVariables $logger.Method.UsingVariables -Splat } } } \ No newline at end of file diff --git a/src/Private/Middleware.ps1 b/src/Private/Middleware.ps1 index 42753964c..8db9d392e 100644 --- a/src/Private/Middleware.ps1 +++ b/src/Private/Middleware.ps1 @@ -38,8 +38,7 @@ function Invoke-PodeMiddleware { } try { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $midware.Arguments -UsingVariables $midware.UsingVariables) - $continue = Invoke-PodeScriptBlock -ScriptBlock $midware.Logic -Arguments $_args -Return -Scoped -Splat + $continue = Invoke-PodeScriptBlock -ScriptBlock $midware.Logic -Arguments $midware.Arguments -UsingVariables $midware.UsingVariables -Return -Scoped -Splat if ($null -eq $continue) { $continue = $true } diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 8b274bccc..69d78310a 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -195,8 +195,7 @@ function Start-PodeWebServer { } } elseif ($null -ne $WebEvent.Route.Logic) { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } @@ -369,8 +368,7 @@ function Start-PodeWebServer { $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name if ($null -ne $SignalEvent.Route) { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat } else { Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index bbc1b1288..a362cee4e 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -91,8 +91,7 @@ function Start-PodeAzFuncServer { } } else { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } @@ -200,8 +199,7 @@ function Start-PodeAwsLambdaServer { } } else { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index 0199a0faa..ef44a28d5 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -20,8 +20,7 @@ function Start-PodeServiceServer { $handlers = Get-PodeHandler -Type Service foreach ($name in $handlers.Keys) { $handler = $handlers[$name] - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $handler.Arguments -UsingVariables $handler.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat } # sleep before next run diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index 050fcdc88..d1fe864bf 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -140,8 +140,7 @@ function Start-PodeSmtpServer { $handlers = Get-PodeHandler -Type Smtp foreach ($name in $handlers.Keys) { $handler = $handlers[$name] - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $handler.Arguments -UsingVariables $handler.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat } } } diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 9b38df87c..e92fac27f 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -149,8 +149,7 @@ function Start-PodeTcpServer { # invoke it if ($null -ne $verb.Logic) { - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $verb.Arguments -UsingVariables $verb.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat } # is the verb auto-close? diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index 95e1d5b2f..e2bc87d45 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -88,11 +88,8 @@ function Invoke-PodeInternalTimer { $_args += $ArgumentList } - # add timer $using args - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $Timer.UsingVariables) - # invoke timer - Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index 96a103164..eef61e50e 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -78,8 +78,7 @@ function Start-PodeWebSocketRunspace { $WsEvent.Files = $result.Files # invoke websocket script - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $websocket.Arguments -UsingVariables $websocket.UsingVariables) - Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $_args -Scoped -Splat + Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat } catch [System.OperationCanceledException] {} catch { diff --git a/src/Public/Access.ps1 b/src/Public/Access.ps1 index fa02559b3..c9da3cbb8 100644 --- a/src/Public/Access.ps1 +++ b/src/Public/Access.ps1 @@ -429,16 +429,14 @@ function Test-PodeAccess { if (($null -eq $Source) -or ($Source.Length -eq 0)) { if ($null -ne $access.Scheme.ScriptBlock) { $_args = $ArgumentList + @($access.Scheme.Arguments) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) - $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -Return -Splat + $Source = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat } } # check for custom validator, or use default match logic if ($null -ne $access.ScriptBlock) { $_args = @(, $Source) + @(, $Destination) + @($access.Arguments) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.ScriptBlock.UsingVariables) - return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -Return -Splat) + return [bool](Invoke-PodeScriptBlock -ScriptBlock $access.ScriptBlock.Script -Arguments $_args -UsingVariables $access.ScriptBlock.UsingVariables -Return -Splat) } # not authorised if no source values @@ -528,8 +526,7 @@ function Test-PodeAccessUser { # otherwise, invoke scriptblock else { $_args = @($user) + @($access.Scheme.Arguments) - $_args = @(Merge-PodeScriptblockArguments -ArgumentList $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables) - $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -Return -Splat + $userAccess = Invoke-PodeScriptBlock -ScriptBlock $access.Scheme.Scriptblock.Script -Arguments $_args -UsingVariables $access.Scheme.Scriptblock.UsingVariables -Return -Splat } # is the user authorised? diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 5c7521d60..450132ee0 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -1170,8 +1170,7 @@ function Add-PodeRouteGroup { } # add routes - $_args = @(Merge-PodeScriptblockArguments -UsingVariables $usingVars) - $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -Arguments $_args -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat } <# @@ -1425,8 +1424,7 @@ function Add-PodeStaticRouteGroup { } # add routes - $_args = @(Merge-PodeScriptblockArguments -UsingVariables $usingVars) - $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -Arguments $_args -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat } @@ -1506,8 +1504,7 @@ function Add-PodeSignalRouteGroup { } # add routes - $_args = @(Merge-PodeScriptblockArguments -UsingVariables $usingVars) - $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -Arguments $_args -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $Routes -UsingVariables $usingVars -Splat } <# diff --git a/src/Public/ScopedVariables.ps1 b/src/Public/ScopedVariables.ps1 index 6b7725c24..55549bfd1 100644 --- a/src/Public/ScopedVariables.ps1 +++ b/src/Public/ScopedVariables.ps1 @@ -1,3 +1,27 @@ +<# +.SYNOPSIS +Converts Scoped Variables within a given ScriptBlock. + +.DESCRIPTION +Converts Scoped Variables within a given ScriptBlock, and returns the updated ScriptBlock back, including any +using-variable values that will need to be supplied as parameters to the ScriptBlock first. + +.PARAMETER ScriptBlock +The ScriptBlock to be converted. + +.PARAMETER PSSession +An optional SessionState object, used to retrieve using-variable values. +If not supplied, using-variable values will not be converted. + +.PARAMETER Exclude +An optional array of one or more Scoped Variable Names to Exclude from converting. (ie: Session, Using, or a Name from Add-PodeScopedVariable) + +.EXAMPLE +$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + +.EXAMPLE +$ScriptBlock = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -Exclude Session, Using +#> function Convert-PodeScopedVariables { [CmdletBinding()] param( @@ -46,6 +70,29 @@ function Convert-PodeScopedVariables { return $ScriptBlock } +<# +.SYNOPSIS +Converts a Scoped Variable within a given ScriptBlock. + +.DESCRIPTION +Converts a Scoped Variable within a given ScriptBlock, and returns the updated ScriptBlock back, including any +other values that will need to be supplied as parameters to the ScriptBlock first. + +.PARAMETER Name +The Name of the Scoped Variable to convert. (ie: Session, Using, or a Name from Add-PodeScopedVariable) + +.PARAMETER ScriptBlock +The ScriptBlock to be converted. + +.PARAMETER PSSession +An optional SessionState object, used to retrieve using-variable values or other values where scope is required. + +.EXAMPLE +$ScriptBlock = Convert-PodeScopedVariable -Name State -ScriptBlock $ScriptBlock + +.EXAMPLE +$ScriptBlock, $otherResults = Convert-PodeScopedVariable -Name Using -ScriptBlock $ScriptBlock +#> function Convert-PodeScopedVariable { [CmdletBinding()] param( @@ -77,7 +124,12 @@ function Convert-PodeScopedVariable { # scriptblock or replace? if ($null -ne $scopedVar.ScriptBlock) { - return (Invoke-PodeScriptBlock -ScriptBlock $scopedVar.ScriptBlock -Arguments $ScriptBlock, $PSSession -Splat -Return -NoNewClosure) + return Invoke-PodeScriptBlock ` + -ScriptBlock $scopedVar.ScriptBlock ` + -Arguments $ScriptBlock, $PSSession, $scopedVar.Get.Pattern, $scopedVar.Set.Pattern ` + -Splat ` + -Return ` + -NoNewClosure } # replace style @@ -108,6 +160,50 @@ function Convert-PodeScopedVariable { } } +<# +.SYNOPSIS +Adds a new Scoped Variable. + +.DESCRIPTION +Adds a new Scoped Variable, to make calling certain functions simpler. +For example "$state:Name" instead of "Get-PodeState" and "Set-PodeState". + +.PARAMETER Name +The Name of the Scoped Variable. + +.PARAMETER GetReplace +A template to be used when converting "$var = $SV:" to a "Get-SVValue -Name " syntax. +You can use the "{{name}}" placeholder to show where the would be placed in the conversion. The result will also be automatically wrapped in brackets. +For example, "$var = $state:" to "Get-PodeState -Name " would need a GetReplace value of "Get-PodeState -Name '{{name}}'". + +.PARAMETER SetReplace +A template to be used when converting "$SV: = " to a "Set-SVValue -Name -Value " syntax. +You can use the "{{name}}" placeholder to show where the would be placed in the conversion. The will automatically be appended to the end. +For example, "$state: = " to "Set-PodeState -Name -Value " would need a SetReplace value of "Set-PodeState -Name '{{name}}' -Value ". + +.PARAMETER ScriptBlock +For more advanced conversions, that aren't as simple as a simple find/replace, you can supply a ScriptBlock instead. +This ScriptBlock will be supplied ScriptBlock to convert, followed by a SessionState object, and the Get/Set regex patterns, as parameters. +The ScriptBlock should returned a converted ScriptBlock that works, plus an optional array of values that should be supplied to the ScriptBlock when invoked. + +.EXAMPLE +Add-PodeScopedVariable -Name 'cache' -SetReplace "Set-PodeCache -Key '{{name}}' -InputObject " -GetReplace "Get-PodeCache -Key '{{name}}'" + +.EXAMPLE +Add-PodeScopedVariable -Name 'config' -ScriptBlock { + param($ScriptBlock, $SessionState, $GetPattern, $SetPattern) + $strScriptBlock = "$($ScriptBlock)" + $template = "(Get-PodeConfig).'{{name}}'" + + # allows "$port = $config:port" instead of "$port = (Get-PodeConfig).port" + while ($strScriptBlock -imatch $GetPattern) { + $getReplace = $template.Replace('{{name}}', $Matches['name']) + $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))") + } + + return [scriptblock]::Create($strScriptBlock) +} +#> function Add-PodeScopedVariable { [CmdletBinding(DefaultParameterSetName = 'Replace')] param( @@ -117,11 +213,11 @@ function Add-PodeScopedVariable { [Parameter(Mandatory = $true, ParameterSetName = 'Replace')] [string] - $SetReplace, + $GetReplace, [Parameter(Mandatory = $true, ParameterSetName = 'Replace')] [string] - $GetReplace, + $SetReplace, [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')] [scriptblock] @@ -137,17 +233,30 @@ function Add-PodeScopedVariable { $PodeContext.Server.ScopedVariables[$Name] = @{ $Name = $Name ScriptBlock = $ScriptBlock - Set = @{ - Pattern = "(?\`$$($Name)\:(?[a-z0-9_\?]+)\s*=)" - Replace = $SetReplace - } Get = @{ Pattern = "(?\`$$($Name)\:(?[a-z0-9_\?]+))" Replace = $GetReplace } + Set = @{ + Pattern = "(?\`$$($Name)\:(?[a-z0-9_\?]+)\s*=)" + Replace = $SetReplace + } } } +<# +.SYNOPSIS +Removes a Scoped Variable. + +.DESCRIPTION +Removes a Scoped Variable. + +.PARAMETER Name +The Name of a Scoped Variable to remove. + +.EXAMPLE +Remove-PodeScopedVariable -Name State +#> function Remove-PodeScopedVariable { [CmdletBinding()] param( @@ -159,6 +268,19 @@ function Remove-PodeScopedVariable { $null = $PodeContext.Server.ScopedVariables.Remove($Name) } +<# +.SYNOPSIS +Tests if a Scoped Variable exists. + +.DESCRIPTION +Tests if a Scoped Variable exists. + +.PARAMETER Name +The Name of the Scoped Variable to check. + +.EXAMPLE +if (Test-PodeScopedVariable -Name $Name) { ... } +#> function Test-PodeScopedVariable { [CmdletBinding()] param( @@ -170,10 +292,36 @@ function Test-PodeScopedVariable { return $PodeContext.Server.ScopedVariables.Contains($Name) } +<# +.SYNOPSIS +Removes all Scoped Variables. + +.DESCRIPTION +Removes all Scoped Variables. + +.EXAMPLE +Clear-PodeScopedVariables +#> function Clear-PodeScopedVariables { $null = $PodeContext.Server.ScopedVariables.Clear() } +<# +.SYNOPSIS +Get a Scoped Variable(s). + +.DESCRIPTION +Get a Scoped Variable(s). + +.PARAMETER Name +The Name of the Scoped Variable(s) to retrieve. + +.EXAMPLE +Get-PodeScopedVariable -Name State + +.EXAMPLE +Get-PodeScopedVariable -Name State, Using +#> function Get-PodeScopedVariable { [CmdletBinding()] param( @@ -193,6 +341,22 @@ function Get-PodeScopedVariable { }) } +<# +.SYNOPSIS +Automatically loads Scoped Variable ps1 files + +.DESCRIPTION +Automatically loads Scoped Variable ps1 files from either a /scoped-vars folder, or a custom folder. Saves space dot-sourcing them all one-by-one. + +.PARAMETER Path +Optional Path to a folder containing ps1 files, can be relative or literal. + +.EXAMPLE +Use-PodeScopedVariables + +.EXAMPLE +Use-PodeScopedVariables -Path './my-vars' +#> function Use-PodeScopedVariables { [CmdletBinding()] param( diff --git a/src/Public/Utilities.ps1 b/src/Public/Utilities.ps1 index 0fc6d2995..5ba286b9b 100644 --- a/src/Public/Utilities.ps1 +++ b/src/Public/Utilities.ps1 @@ -475,6 +475,9 @@ The ScriptBlock to invoke. .PARAMETER Arguments Any arguments that should be supplied to the ScriptBlock. +.PARAMETER UsingVariables +Optional array of "using-variable" values, which will be automatically prepended to any supplied Arguments when supplied to the ScriptBlock. + .PARAMETER Scoped Run the ScriptBlock in a scoped context. @@ -504,6 +507,10 @@ function Invoke-PodeScriptBlock { [Parameter()] $Arguments = $null, + [Parameter()] + [object[]] + $UsingVariables = $null, + [switch] $Scoped, @@ -517,14 +524,22 @@ function Invoke-PodeScriptBlock { $NoNewClosure ) + # force no new closure if running serverless if ($PodeContext.Server.IsServerless) { $NoNewClosure = $true } + # if new closure needed, create it if (!$NoNewClosure) { $ScriptBlock = ($ScriptBlock).GetNewClosure() } + # merge arguments together, if we have using vars supplied + if (($null -ne $UsingVariables) -and ($UsingVariables.Length -gt 0)) { + $Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVariables $UsingVariables) + } + + # invoke the scriptblock if ($Scoped) { if ($Splat) { $result = (& $ScriptBlock @Arguments) @@ -542,20 +557,41 @@ function Invoke-PodeScriptBlock { } } + # if needed, return the result if ($Return) { return $result } } +<# +.SYNOPSIS +Merges Arguments and Using Variables together. + +.DESCRIPTION +Merges Arguments and Using Variables together to be supplied to a ScriptBlock. +The Using Variables will be prepended so then are supplied first to a ScriptBlock. + +.PARAMETER ArgumentList +And optional array of Arguments. + +.PARAMETER UsingVariables +And optional array of "using-variable" values to be prepended. + +.EXAMPLE +$Arguments = @(Merge-PodeScriptblockArguments -ArgumentList $Arguments -UsingVariables $UsingVariables) + +.EXAMPLE +$Arguments = @(Merge-PodeScriptblockArguments -UsingVariables $UsingVariables) +#> function Merge-PodeScriptblockArguments { param( [Parameter()] [object[]] - $ArgumentList, + $ArgumentList = $null, [Parameter()] [object[]] - $UsingVariables + $UsingVariables = $null ) if ($null -eq $ArgumentList) { From cea2044dadc02500b80fb3c3c49826e8eb65aba8 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sat, 10 Feb 2024 19:05:44 +0000 Subject: [PATCH 17/84] #1207: add scoped vars docs, making -SetReplace optional --- docs/Tutorials/ScopedVariables.md | 99 +++++++++++++++++++++++++++++++ docs/Tutorials/Scoping.md | 13 ++++ src/Public/ScopedVariables.ps1 | 16 ++--- 3 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 docs/Tutorials/ScopedVariables.md diff --git a/docs/Tutorials/ScopedVariables.md b/docs/Tutorials/ScopedVariables.md new file mode 100644 index 000000000..06e7719b7 --- /dev/null +++ b/docs/Tutorials/ScopedVariables.md @@ -0,0 +1,99 @@ +# Scoped Variables + +You can create custom Scoped Variables within Pode, to allow for easier/quicker access to values without having to supply function calls every time within ScriptBlocks - such as those supplied to Routes, Middleware, etc. + +For example, the inbuilt `$state:` Scoped Variable is a quick way of calling `Get-PodeState` and `Set-PodeState`, but without having to write out those functions constantly! + +Pode has support for the following inbuilt Scoped Variables: + +* `$cache:` +* `$secret:` +* `$session:` +* `$state:` +* `$using:` + +The `$using:` Scoped Variable is a special case, as it only allows for the retrieval of a value, and not the setting of the value as well. + +## Creation + +To create a custom Scoped Variable you can use [`Add-PodeScopedVariable`](../../Functions/ScopedVariables/Add-PodeScopedVariable) with a unique Name. There are two ways to add a Scoped Variable: + +* A [simple](#simple-replace) Replacement conversion from `$var:` syntax to Get/Set function syntax. +* A more [advanced](#advanced) conversion strategy using a ScriptBlock. + +### Simple Replace + +The simple Replacement conversion using [`Add-PodeScopedVariable`](../../Functions/ScopedVariables/Add-PodeScopedVariable) requires you to supply a `-GetReplace` and an optional `-SetReplace` template strings. These template strings will be used appropriately replace `$var:` calls with the template Get/Set function calls. + +Within the template strings there is a special placeholder, `{{name}}`, which can be used. This placeholder is where the "name" from `$var:` will be used within the Get/Set replacement. + +Using the inbuilt `$state` Scoped Variable as an example, this conversion is done using the Get/Set replacement method. For this Scoped Variable we want: + +```powershell +$value = $state:Name +# to be replaced with +$value = (Get-PodeState -Name 'Name') + +$state:Name = 'Value' +# to be replace with +Set-PodeState -Name 'Name' -Value 'Value' +``` + +to achieve this, we can call [`Add-PodeScopedVariable`](../../Functions/ScopedVariables/Add-PodeScopedVariable) as follows: + +```powershell +Add-PodeScopedVariable -Name 'state' ` + -SetReplace "Set-PodeState -Name '{{name}}' -Value " ` + -GetReplace "Get-PodeState -Name '{{name}}'" +``` + +### Advanced + +A more advanced conversion can be achieved using [`Add-PodeScopedVariable`](../../Functions/ScopedVariables/Add-PodeScopedVariable) by supplying a `-ScriptBlock` instead of the Replace parameters. This ScriptBlock will be supplied with: + +* The ScriptBlock that needs converting. +* A SessionState object for when scoping is required for retrieving values (like `$using:`). +* A "Get" pattern which can be used for finding `$var:Name` syntaxes within the supplied ScriptBlock. +* A "Set" pattern which can be used for finding `$value = $var:Name` syntaxes within the supplied ScriptBlock. + +The ScriptBlock supplied to [`Add-PodeScopedVariable`](../../Functions/ScopedVariables/Add-PodeScopedVariable) should return a converted version of the ScriptBlock supplied to it. It should also return an optional array of values which need to be supplied to the converted ScriptBlock first. + +For example, if you wanted to add a custom `$config:` Scoped Variable, to simplify calling [`Get-PodeConfig`](../../Functions/Utilities/Get-PodeConfig), but you wanted to do this using [`Add-PodeScopedVariable`](../../Functions/ScopedVariables/Add-PodeScopedVariable)'s `-ScriptBlock` instead of the Replacement parameters, then you could do the following: + +```powershell +Add-PodeScopedVariable -Name 'config' -ScriptBlock { + param($ScriptBlock, $SessionState, $GetPattern, $SetPattern) + + # convert the scriptblock to a string, for searching + $strScriptBlock = "$($ScriptBlock)" + + # the "get" template to be used, to convert "$config:Name" syntax to "(Get-PodeConfig).Name" + $template = "(Get-PodeConfig).'{{name}}'" + + # loop through the scriptblock, replacing "$config:Name" syntax + while ($strScriptBlock -imatch $GetPattern) { + $getReplace = $template.Replace('{{name}}', $Matches['name']) + $strScriptBlock = $strScriptBlock.Replace($Matches['full'], "($($getReplace))") + } + + # convert string back to scriptblock, and return + return [scriptblock]::Create($strScriptBlock) +} +``` + +## Conversion + +If you have a ScriptBlock that you need to convert, in an ad-hoc manner, you can manually call [`Convert-PodeScopedVariables`](../../Functions/ScopedVariables/Convert-PodeScopedVariables) yourself. You should supply the `-ScriptBlock` to wish to convert, as well as an optional `-PSSession` SessionState from `$PSCmdlet.SessionState`, to this function: + +```powershell +# convert the scriptblock's scoped variables +$ScriptBlock, $usingVars = Convert-PodeScopedVariables -ScriptBlock $ScriptBlock -PSSession $PSCmdlet.SessionState + +# invoke the converted scriptblock, and supply any using variable values +$result = Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -UsingVariables $usingVars -Splat -Return +``` + +!!! note + If you don't supply a `-PSSession` then no `$using:` Scoped Variables will be converted. + +You can also supply one or more Scoped Variable Names to `-Exclude`, which will skip over converting these Scoped Variables in the supplied `-ScriptBlock`. diff --git a/docs/Tutorials/Scoping.md b/docs/Tutorials/Scoping.md index c11b7e44b..dfa1d3728 100644 --- a/docs/Tutorials/Scoping.md +++ b/docs/Tutorials/Scoping.md @@ -235,13 +235,20 @@ Prior to 2.0 if you wanted to use quick local variables in your Routes/etc, you The `$using:` syntax is supported in almost all `-ScriptBlock` parameters for the likes of: * Authentication +* Access +* Caching * Endware +* Events +* FileWatchers * Handlers * Logging * Middleware * Routes * Schedules +* Secrets * Timers +* Verbs +* WebSockets Below, the `$outer_msg` and `$inner_msg` variables can now be more simply referenced in a Route: @@ -259,6 +266,12 @@ Start-PodeServer -ScriptBlock { } ``` +!!! note + In some environments, or for some use cases like Pode.Web, the `$outer_msg` variables need to be created within the `Start-PodeServer` scriptblock due to scoping issues. + +!!! tip + You can create custom Scoped Variables like `$using:`, `$state:`, and `$session` using the documentation for [Scoped Variables](../ScopedVariables). + ## Secret Vaults This mostly only applies to vaults registered via the SecretManagement module, and its `Register-SecretVault` function. You can register vaults as the user that will run your Pode server, before you start the server using the normal `Register-SecretVault` function. On start, Pode will detect these vaults and will automatically register then within Pode for you - ready to be used for mounting secrets with [`Mount-PodeSecret`](../../Functions/Secrets/Mount-PodeSecret). diff --git a/src/Public/ScopedVariables.ps1 b/src/Public/ScopedVariables.ps1 index 55549bfd1..7a92a1149 100644 --- a/src/Public/ScopedVariables.ps1 +++ b/src/Public/ScopedVariables.ps1 @@ -143,10 +143,12 @@ function Convert-PodeScopedVariable { return $ScriptBlock } - # loop and replace "set" syntax - while ($strScriptBlock -imatch $scopedVar.Set.Pattern) { - $setReplace = $scopedVar.Set.Replace.Replace('{{name}}', $Matches['name']) - $strScriptBlock = $strScriptBlock.Replace($Matches['full'], $setReplace) + # loop and replace "set" syntax if replace template supplied + if (![string]::IsNullOrEmpty($scopedVar.Set.Replace)) { + while ($strScriptBlock -imatch $scopedVar.Set.Pattern) { + $setReplace = $scopedVar.Set.Replace.Replace('{{name}}', $Matches['name']) + $strScriptBlock = $strScriptBlock.Replace($Matches['full'], $setReplace) + } } # loop and replace "get" syntax @@ -177,7 +179,7 @@ You can use the "{{name}}" placeholder to show where the would be placed For example, "$var = $state:" to "Get-PodeState -Name " would need a GetReplace value of "Get-PodeState -Name '{{name}}'". .PARAMETER SetReplace -A template to be used when converting "$SV: = " to a "Set-SVValue -Name -Value " syntax. +An optional template to be used when converting "$SV: = " to a "Set-SVValue -Name -Value " syntax. You can use the "{{name}}" placeholder to show where the would be placed in the conversion. The will automatically be appended to the end. For example, "$state: = " to "Set-PodeState -Name -Value " would need a SetReplace value of "Set-PodeState -Name '{{name}}' -Value ". @@ -215,9 +217,9 @@ function Add-PodeScopedVariable { [string] $GetReplace, - [Parameter(Mandatory = $true, ParameterSetName = 'Replace')] + [Parameter(ParameterSetName = 'Replace')] [string] - $SetReplace, + $SetReplace = $null, [Parameter(Mandatory = $true, ParameterSetName = 'ScriptBlock')] [scriptblock] From 9d5fd7ce34d39cd20d9cef64540027df7749819e Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 11 Feb 2024 12:27:33 +0000 Subject: [PATCH 18/84] #1207: add null= for invoking a scriptblock, to ensure nothing is returned --- src/Private/FileWatchers.ps1 | 2 +- src/Private/PodeServer.ps1 | 4 ++-- src/Private/Secrets.ps1 | 8 ++++---- src/Private/Server.ps1 | 2 +- src/Private/Serverless.ps1 | 4 ++-- src/Private/ServiceServer.ps1 | 2 +- src/Private/Sessions.ps1 | 6 +++--- src/Private/SmtpServer.ps1 | 2 +- src/Private/TcpServer.ps1 | 2 +- src/Private/Timers.ps1 | 2 +- src/Private/WebSockets.ps1 | 2 +- src/Public/Caching.ps1 | 6 +++--- src/Public/Sessions.ps1 | 2 +- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index 2bc7e6788..85bc88c81 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -99,7 +99,7 @@ function Start-PodeFileWatcherRunspace { } # invoke main script - Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $fileWatcher.Script -Arguments $fileWatcher.Arguments -UsingVariables $fileWatcher.UsingVariables -Scoped -Splat } catch [System.OperationCanceledException] { } diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 69d78310a..0387c6cc3 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -195,7 +195,7 @@ function Start-PodeWebServer { } } elseif ($null -ne $WebEvent.Route.Logic) { - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } @@ -368,7 +368,7 @@ function Start-PodeWebServer { $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name if ($null -ne $SignalEvent.Route) { - Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $SignalEvent.Route.Logic -Arguments $SignalEvent.Route.Arguments -UsingVariables $SignalEvent.Route.UsingVariables -Scoped -Splat } else { Send-PodeSignal -Value $SignalEvent.Data.Message -Path $SignalEvent.Data.Path -ClientId $SignalEvent.Data.ClientId diff --git a/src/Private/Secrets.ps1 b/src/Private/Secrets.ps1 index ebf15abce..612564fa2 100644 --- a/src/Private/Secrets.ps1 +++ b/src/Private/Secrets.ps1 @@ -9,7 +9,7 @@ function Initialize-PodeSecretVault { $ScriptBlock ) - Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters) + $null = Invoke-PodeScriptBlock -ScriptBlock $ScriptBlock -Splat -Arguments @($VaultConfig.Parameters) } function Register-PodeSecretManagementVault { @@ -180,7 +180,7 @@ function Unregister-PodeSecretCustomVault { } # unregister the vault - Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @( + $null = Invoke-PodeScriptBlock -ScriptBlock $VaultConfig.Custom.Unregister -Splat -Arguments @( $VaultConfig.Parameters ) } @@ -286,7 +286,7 @@ function Set-PodeSecretCustomKey { } # set the secret - Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Set -Splat -Arguments (@( + $null = Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Set -Splat -Arguments (@( $_vault.Parameters, $Key, $Value, @@ -336,7 +336,7 @@ function Remove-PodeSecretCustomKey { } # remove the secret - Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Remove -Splat -Arguments (@( + $null = Invoke-PodeScriptBlock -ScriptBlock $_vault.Custom.Remove -Splat -Arguments (@( $_vault.Parameters, $Key ) + $ArgumentList) diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 2b140d089..d2408a1f0 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -30,7 +30,7 @@ function Start-PodeInternalServer { } $_script = Convert-PodeScopedVariables -ScriptBlock $_script -Exclude Session, Using - Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $_script -NoNewClosure -Splat # load any modules/snapins Import-PodeSnapinsIntoRunspaceState diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index a362cee4e..eceefa0da 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -91,7 +91,7 @@ function Start-PodeAzFuncServer { } } else { - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } @@ -199,7 +199,7 @@ function Start-PodeAwsLambdaServer { } } else { - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index ef44a28d5..7e0d66b2d 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -20,7 +20,7 @@ function Start-PodeServiceServer { $handlers = Get-PodeHandler -Type Service foreach ($name in $handlers.Keys) { $handler = $handlers[$name] - Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat } # sleep before next run diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1 index 61ed58f28..20da1f15b 100644 --- a/src/Private/Sessions.ps1 +++ b/src/Private/Sessions.ps1 @@ -113,7 +113,7 @@ function Revoke-PodeSession { } # remove session from store - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Delete + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Delete # blank the session $WebEvent.Session.Clear() @@ -182,7 +182,7 @@ function Set-PodeSessionHelpers { } # save session data to store - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($session.Id, $data, $expiry) -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($session.Id, $data, $expiry) -Splat # update session's data hash Set-PodeSessionDataHash @@ -194,7 +194,7 @@ function Set-PodeSessionHelpers { $session = $WebEvent.Session # remove data from store - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $session.Id + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $session.Id # clear session $session.Clear() diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index d1fe864bf..e08d7cbfa 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -140,7 +140,7 @@ function Start-PodeSmtpServer { $handlers = Get-PodeHandler -Type Smtp foreach ($name in $handlers.Keys) { $handler = $handlers[$name] - Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $handler.Logic -Arguments $handler.Arguments -UsingVariables $handler.UsingVariables -Scoped -Splat } } } diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index e92fac27f..0581b1f78 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -149,7 +149,7 @@ function Start-PodeTcpServer { # invoke it if ($null -ne $verb.Logic) { - Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $verb.Logic -Arguments $verb.Arguments -UsingVariables $verb.UsingVariables -Scoped -Splat } # is the verb auto-close? diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index e2bc87d45..a6f1136bf 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -89,7 +89,7 @@ function Invoke-PodeInternalTimer { } # invoke timer - Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $Timer.Script -Arguments $_args -UsingVariables $Timer.UsingVariables -Scoped -Splat } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index eef61e50e..16fbf5cff 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -78,7 +78,7 @@ function Start-PodeWebSocketRunspace { $WsEvent.Files = $result.Files # invoke websocket script - Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $websocket.Logic -Arguments $websocket.Arguments -UsingVariables $websocket.UsingVariables -Scoped -Splat } catch [System.OperationCanceledException] {} catch { diff --git a/src/Public/Caching.ps1 b/src/Public/Caching.ps1 index df7972b19..30fff5c41 100644 --- a/src/Public/Caching.ps1 +++ b/src/Public/Caching.ps1 @@ -134,7 +134,7 @@ function Set-PodeCache { # used custom storage elseif (Test-PodeCacheStorage -Key $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Set -Arguments @($Key, $InputObject, $Ttl) -Splat } # storage not found! @@ -236,7 +236,7 @@ function Remove-PodeCache { # used custom storage elseif (Test-PodeCacheStorage -Key $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Remove -Arguments @($Key) -Splat } # storage not found! @@ -281,7 +281,7 @@ function Clear-PodeCache { # used custom storage elseif (Test-PodeCacheStorage -Key $Storage) { - Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Cache.Storage[$Storage].Clear } # storage not found! diff --git a/src/Public/Sessions.ps1 b/src/Public/Sessions.ps1 index b8335452b..c7b487b38 100644 --- a/src/Public/Sessions.ps1 +++ b/src/Public/Sessions.ps1 @@ -210,7 +210,7 @@ function Save-PodeSession { } # save the session - Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Save -Arguments @($Force.IsPresent) -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Save -Arguments @($Force.IsPresent) -Splat } <# From 45a953d6ae1437570091bc343ad7ae74aaaa1ca6 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 11 Feb 2024 13:13:58 +0000 Subject: [PATCH 19/84] Add missing HTTP 425 response --- src/Private/Mappers.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Private/Mappers.ps1 b/src/Private/Mappers.ps1 index a16b6c05d..2590a2b5a 100644 --- a/src/Private/Mappers.ps1 +++ b/src/Private/Mappers.ps1 @@ -698,6 +698,7 @@ function Get-PodeStatusDescription { 422 { return 'Unprocessable Entity' } 423 { return 'Locked' } 424 { return 'Failed Dependency' } + 425 { return 'Too Early' } 426 { return 'Upgrade Required' } 428 { return 'Precondition Required' } 429 { return 'Too Many Requests' } From bc9137d02d4682743b75c9dfe10ace2b34ff64e3 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Mon, 19 Feb 2024 09:01:32 +0000 Subject: [PATCH 20/84] #1241: initial work for enabling Tab Sessions --- examples/web-auth-form.ps1 | 6 +- src/Pode.psd1 | 4 + src/Private/Sessions.ps1 | 157 ++++++++++++++++++++++++++-------- src/Public/Authentication.ps1 | 14 +-- src/Public/Flash.ps1 | 12 +-- src/Public/Middleware.ps1 | 2 +- src/Public/Sessions.ps1 | 51 +++++++++-- tests/unit/Security.Tests.ps1 | 4 +- tests/unit/Sessions.Tests.ps1 | 12 +-- 9 files changed, 192 insertions(+), 70 deletions(-) diff --git a/examples/web-auth-form.ps1 b/examples/web-auth-form.ps1 index 6dda283a2..e9c5e57f9 100644 --- a/examples/web-auth-form.ps1 +++ b/examples/web-auth-form.ps1 @@ -37,7 +37,7 @@ Start-PodeServer -Threads 2 { if ($username -eq 'morty' -and $password -eq 'pickle') { return @{ User = @{ - ID ='M0R7Y302' + ID = 'M0R7Y302' Name = 'Morty' Type = 'Human' } @@ -55,8 +55,8 @@ Start-PodeServer -Threads 2 { Write-PodeViewResponse -Path 'auth-home' -Data @{ Username = $WebEvent.Auth.User.Name - Views = $session:Views - Expiry = Get-PodeSessionExpiry + Views = $session:Views + Expiry = Get-PodeSessionExpiry } } diff --git a/src/Pode.psd1 b/src/Pode.psd1 index c73165e3d..f722c1c92 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -204,6 +204,10 @@ 'Reset-PodeSessionExpiry', 'Get-PodeSessionDuration', 'Get-PodeSessionExpiry', + 'Test-PodeSessionsEnabled', + 'Get-PodeSessionTabId', + 'Get-PodeSessionInfo', + 'Test-PodeSessionScopeIsBrowser', # auth 'New-PodeAuthScheme', diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1 index 20da1f15b..32c8642dd 100644 --- a/src/Private/Sessions.ps1 +++ b/src/Private/Sessions.ps1 @@ -1,13 +1,43 @@ function New-PodeSession { + # sessionId + $sessionId = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.GenerateId -Return + + # tabId + $tabId = $null + if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser) { + $tabId = Get-PodeSessionTabId + } + + # return new session data return @{ Name = $PodeContext.Server.Sessions.Name - Id = (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.GenerateId -Return) + Id = $sessionId + TabId = $tabId + FullId = (Get-PodeSessionFullId -SessionId $sessionId -TabId $tabId) Extend = $PodeContext.Server.Sessions.Info.Extend TimeStamp = [datetime]::UtcNow Data = @{} } } +function Get-PodeSessionFullId { + param( + [Parameter()] + [string] + $SessionId, + + [Parameter()] + [string] + $TabId + ) + + if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and ![string]::IsNullOrEmpty($TabId)) { + return "$($SessionId)-$($TabId)" + } + + return $SessionId +} + function ConvertTo-PodeSessionStrictSecret { param( [Parameter(Mandatory = $true)] @@ -23,9 +53,8 @@ function Set-PodeSession { throw 'there is no session available to set on the response' } + # convert secret to strict mode $secret = $PodeContext.Server.Sessions.Secret - - # covert secret to strict mode if ($PodeContext.Server.Sessions.Info.Strict) { $secret = ConvertTo-PodeSessionStrictSecret -Secret $secret } @@ -49,10 +78,11 @@ function Set-PodeSession { function Get-PodeSession { $secret = $PodeContext.Server.Sessions.Secret - $value = $null + $sessionId = $null + $tabId = Get-PodeSessionTabId $name = $PodeContext.Server.Sessions.Name - # covert secret to strict mode + # convert secret to strict mode if ($PodeContext.Server.Sessions.Info.Strict) { $secret = ConvertTo-PodeSessionStrictSecret -Secret $secret } @@ -65,8 +95,8 @@ function Get-PodeSession { } # get the header from the request - $value = Get-PodeHeader -Name $PodeContext.Server.Sessions.Name -Secret $secret - if ([string]::IsNullOrWhiteSpace($value)) { + $sessionId = Get-PodeHeader -Name $PodeContext.Server.Sessions.Name -Secret $secret + if ([string]::IsNullOrEmpty($sessionId)) { return $null } } @@ -80,19 +110,21 @@ function Get-PodeSession { # get the cookie from the request $cookie = Get-PodeCookie -Name $PodeContext.Server.Sessions.Name -Secret $secret - if ([string]::IsNullOrWhiteSpace($cookie)) { + if ([string]::IsNullOrEmpty($cookie)) { return $null } # get details from cookie $name = $cookie.Name - $value = $cookie.Value + $sessionId = $cookie.Value } # generate the session data $data = @{ Name = $name - Id = $value + Id = $sessionId + TabId = $tabId + FullId = (Get-PodeSessionFullId -SessionId $sessionId -TabId $tabId) Extend = $PodeContext.Server.Sessions.Info.Extend TimeStamp = $null Data = @{} @@ -107,7 +139,7 @@ function Revoke-PodeSession { return } - # remove from cookie + # remove from cookie if being used if (!$PodeContext.Server.Sessions.Info.UseHeaders) { Remove-PodeCookie -Name $WebEvent.Session.Name } @@ -157,32 +189,47 @@ function Set-PodeSessionHelpers { $WebEvent.Session | Add-Member -MemberType NoteProperty -Name Save -Value { param($check) - # the current session - $session = $WebEvent.Session - # do nothing if session has no ID - if ([string]::IsNullOrWhiteSpace($session.Id)) { + if ([string]::IsNullOrEmpty($WebEvent.Session.FullId)) { return } # only save if check and hashes different, but not if extending expiry or updated - if (!$session.Extend -and $check -and (Test-PodeSessionDataHash)) { + if (!$WebEvent.Session.Extend -and $check -and (Test-PodeSessionDataHash)) { return } # generate the expiry $expiry = Get-PodeSessionExpiry - # the data to save - which will be the data, some extra metadata + # the data to save - which will be the data, and some extra metadata like timestamp $data = @{ + Version = 3 Metadata = @{ - TimeStamp = $session.TimeStamp + TimeStamp = $WebEvent.Session.TimeStamp + } + Data = $WebEvent.Session.Data + } + + # save base session data to store + if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and $WebEvent.Session.TabId) { + $authData = @{ + Version = 3 + Metadata = @{ + TimeStamp = $WebEvent.Session.TimeStamp + Tabbed = $true + } + Data = @{ + Auth = $WebEvent.Session.Data.Auth + } } - Data = $session.Data + + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.Id, $authData, $expiry) -Splat + $data.Metadata['Parent'] = $WebEvent.Session.Id } # save session data to store - $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($session.Id, $data, $expiry) -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.FullId, $data, $expiry) -Splat # update session's data hash Set-PodeSessionDataHash @@ -190,14 +237,11 @@ function Set-PodeSessionHelpers { # delete the current session $WebEvent.Session | Add-Member -MemberType NoteProperty -Name Delete -Value { - # the current session - $session = $WebEvent.Session - # remove data from store - $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $session.Id + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $WebEvent.Session.Id # clear session - $session.Clear() + $WebEvent.Session.Clear() } } @@ -211,6 +255,9 @@ function Get-PodeSessionInMemStore { $store | Add-Member -MemberType NoteProperty -Name Delete -Value { param($sessionId) $null = $PodeContext.Server.Sessions.Store.Memory.Remove($sessionId) + if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser) { + Invoke-PodeSchedule -Name '__pode_session_inmem_cleanup__' + } } # get a sessionId's data @@ -249,25 +296,30 @@ function Set-PodeSessionInMemClearDown { # cleardown expired inmem session every 10 minutes Add-PodeSchedule -Name '__pode_session_inmem_cleanup__' -Cron '0/10 * * * *' -ScriptBlock { + # do nothing if no sessions $store = $PodeContext.Server.Sessions.Store - if (Test-PodeIsEmpty $store.Memory) { + if (($null -eq $store.Memory) -or ($store.Memory.Count -eq 0)) { return } - # remove sessions that have expired + # remove sessions that have expired, or where the parent is gone $now = [DateTime]::UtcNow foreach ($key in $store.Memory.Keys) { + # expired if ($store.Memory[$key].Expiry -lt $now) { $null = $store.Memory.Remove($key) + continue + } + + # parent check - gone/expired + $parentKey = $store.Memory[$key].Data.Metadata.Parent + if ($parentKey -and (!$store.Memory.ContainsKey($parentKey) -or ($store.Memory[$parentKey].Expiry -lt $now))) { + $null = $store.Memory.Remove($key) } } } } -function Test-PodeSessionsConfigured { - return (($null -ne $PodeContext.Server.Sessions) -and ($PodeContext.Server.Sessions.Count -gt 0)) -} - function Test-PodeSessionsInUse { return (($null -ne $WebEvent.Session) -and ($WebEvent.Session.Count -gt 0)) } @@ -276,10 +328,38 @@ function Get-PodeSessionData { param( [Parameter()] [string] - $SessionId + $SessionId, + + [Parameter()] + [string] + $TabId = $null ) - return (Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $SessionId -Return) + $data = $null + + # try and get Tab session + if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and ![string]::IsNullOrEmpty($TabId)) { + $data = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments "$($SessionId)-$($TabId)" -Return + + # now get the parent - but fail if it doesn't exist + if ($data.Metadata.Parent) { + $parent = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $data.Metadata.Parent -Return + if (!$parent) { + return $null + } + + if (!$data.Data.Auth) { + $data.Data.Auth = $parent.Data.Auth + } + } + } + + # try and get normal session + if (($null -eq $data) -and ![string]::IsNullOrEmpty($SessionId)) { + $data = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Get -Arguments $SessionId -Return + } + + return $data } function Get-PodeSessionMiddleware { @@ -300,14 +380,19 @@ function Get-PodeSessionMiddleware { } # get the session's data from store - elseif ($null -ne ($data = (Get-PodeSessionData -SessionId $WebEvent.Session.Id))) { - if ($null -eq $data.Metadata) { + elseif ($null -ne ($data = (Get-PodeSessionData -SessionId $WebEvent.Session.Id -TabId $WebEvent.Session.TabId))) { + if ($data.Version -lt 3) { $WebEvent.Session.Data = $data $WebEvent.Session.TimeStamp = [datetime]::UtcNow } else { $WebEvent.Session.Data = $data.Data - $WebEvent.Session.TimeStamp = $data.Metadata.TimeStamp + if ($data.Metadata.Tabbed) { + $WebEvent.Session.TimeStamp = [datetime]::UtcNow + } + else { + $WebEvent.Session.TimeStamp = $data.Metadata.TimeStamp + } } } diff --git a/src/Public/Authentication.ps1 b/src/Public/Authentication.ps1 index 0451f5a80..77422d934 100644 --- a/src/Public/Authentication.ps1 +++ b/src/Public/Authentication.ps1 @@ -406,7 +406,7 @@ function New-PodeAuthScheme { throw 'OAuth2 requires an Authorise URL to be supplied' } - if ($UsePKCE -and !(Test-PodeSessionsConfigured)) { + if ($UsePKCE -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use OAuth2 with PKCE' } @@ -749,7 +749,7 @@ function Add-PodeAuth { } # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsConfigured)) { + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use session persistent authentication' } @@ -911,7 +911,7 @@ function Merge-PodeAuth { } # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsConfigured)) { + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use session persistent authentication' } @@ -1242,7 +1242,7 @@ function Add-PodeAuthWindowsAd { } # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsConfigured)) { + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use session persistent authentication' } @@ -1367,7 +1367,7 @@ function Add-PodeAuthSession { ) # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions have not been configured' } @@ -1815,7 +1815,7 @@ function Add-PodeAuthUserFile { } # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsConfigured)) { + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use session persistent authentication' } @@ -1977,7 +1977,7 @@ function Add-PodeAuthWindowsLocal { } # if we're using sessions, ensure sessions have been setup - if (!$Sessionless -and !(Test-PodeSessionsConfigured)) { + if (!$Sessionless -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use session persistent authentication' } diff --git a/src/Public/Flash.ps1 b/src/Public/Flash.ps1 index b646c761e..ca005ea8c 100644 --- a/src/Public/Flash.ps1 +++ b/src/Public/Flash.ps1 @@ -28,7 +28,7 @@ function Add-PodeFlashMessage { ) # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use Flash messages' } @@ -60,7 +60,7 @@ function Clear-PodeFlashMessages { param() # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use Flash messages' } @@ -94,7 +94,7 @@ function Get-PodeFlashMessage { ) # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use Flash messages' } @@ -129,7 +129,7 @@ function Get-PodeFlashMessageNames { param() # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use Flash messages' } @@ -163,7 +163,7 @@ function Remove-PodeFlashMessage { ) # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use Flash messages' } @@ -196,7 +196,7 @@ function Test-PodeFlashMessage { ) # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use Flash messages' } diff --git a/src/Public/Middleware.ps1 b/src/Public/Middleware.ps1 index 9fb4c4809..6ee06f80f 100644 --- a/src/Public/Middleware.ps1 +++ b/src/Public/Middleware.ps1 @@ -238,7 +238,7 @@ function Initialize-PodeCsrf { } # if sessions haven't been setup and we're not using cookies, error - if (!$UseCookies -and !(Test-PodeSessionsConfigured)) { + if (!$UseCookies -and !(Test-PodeSessionsEnabled)) { throw 'Sessions are required to use CSRF unless you want to use cookies' } diff --git a/src/Public/Sessions.ps1 b/src/Public/Sessions.ps1 index c7b487b38..7d865695a 100644 --- a/src/Public/Sessions.ps1 +++ b/src/Public/Sessions.ps1 @@ -74,7 +74,12 @@ function Enable-PodeSessionMiddleware { [Parameter()] [psobject] - $Storage, + $Storage = $null, + + [Parameter()] + [ValidateSet('Browser', 'Tab')] + [string] + $Scope = 'Browser', [switch] $Extend, @@ -96,7 +101,7 @@ function Enable-PodeSessionMiddleware { ) # check that session logic hasn't already been initialised - if (Test-PodeSessionsConfigured) { + if (Test-PodeSessionsEnabled) { throw 'Session Middleware has already been intialised' } @@ -138,12 +143,17 @@ function Enable-PodeSessionMiddleware { Strict = $Strict.IsPresent HttpOnly = $HttpOnly.IsPresent UseHeaders = $UseHeaders.IsPresent + Scope = @{ + Type = $Scope.ToLowerInvariant() + IsBrowser = ($Scope -ieq 'Browser') + } } } # return scriptblock for the session middleware - $script = Get-PodeSessionMiddleware - (New-PodeMiddleware -ScriptBlock $script) | Add-PodeMiddleware -Name '__pode_mw_sessions__' + Get-PodeSessionMiddleware | + New-PodeMiddleware | + Add-PodeMiddleware -Name '__pode_mw_sessions__' } <# @@ -161,11 +171,11 @@ function Remove-PodeSession { param() # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions have not been configured' } - # do nothin if session is null + # do nothing if session is null if ($null -eq $WebEvent.Session) { return } @@ -195,7 +205,7 @@ function Save-PodeSession { ) # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions have not been configured' } @@ -248,7 +258,7 @@ function Get-PodeSessionId { } # get the sessionId - $sessionId = $WebEvent.Session.Id + $sessionId = $WebEvent.Session.FullId # do they want the session signed? if ($Signed) { @@ -268,6 +278,17 @@ function Get-PodeSessionId { return $sessionId } +function Get-PodeSessionTabId { + [CmdletBinding()] + param() + + if ($PodeContext.Server.Sessions.Info.Scope.IsBrowser) { + return $null + } + + return Get-PodeHeader -Name 'X-PODE-SESSION-TAB-ID' +} + <# .SYNOPSIS Resets the current Session's expiry date. @@ -283,7 +304,7 @@ function Reset-PodeSessionExpiry { param() # if sessions haven't been setup, error - if (!(Test-PodeSessionsConfigured)) { + if (!(Test-PodeSessionsEnabled)) { throw 'Sessions have not been configured' } @@ -352,4 +373,16 @@ function Get-PodeSessionExpiry { # return expiry return $expiry +} + +function Test-PodeSessionsEnabled { + return (($null -ne $PodeContext.Server.Sessions) -and ($PodeContext.Server.Sessions.Count -gt 0)) +} + +function Get-PodeSessionInfo { + return $PodeContext.Server.Sessions.Info +} + +function Test-PodeSessionScopeIsBrowser { + return [bool]$PodeContext.Server.Sessions.Info.Scope.IsBrowser } \ No newline at end of file diff --git a/tests/unit/Security.Tests.ps1 b/tests/unit/Security.Tests.ps1 index 912b85291..559d7358d 100644 --- a/tests/unit/Security.Tests.ps1 +++ b/tests/unit/Security.Tests.ps1 @@ -427,7 +427,7 @@ Describe 'Initialize-PodeCsrf' { }}} Mock Test-PodeCsrfConfigured { return $false } - Mock Test-PodeSessionsConfigured { return $true } + Mock Test-PodeSessionsEnabled { return $true } Mock Get-PodeCookieSecret { return 'secret' } Initialize-PodeCsrf -IgnoreMethods @('Get') @@ -444,7 +444,7 @@ Describe 'Initialize-PodeCsrf' { }}} Mock Test-PodeCsrfConfigured { return $false } - Mock Test-PodeSessionsConfigured { return $false } + Mock Test-PodeSessionsEnabled { return $false } Mock Get-PodeCookieSecret { return 'secret' } Initialize-PodeCsrf -IgnoreMethods @('Get') -UseCookies diff --git a/tests/unit/Sessions.Tests.ps1 b/tests/unit/Sessions.Tests.ps1 index 7e685a89d..8201792a0 100644 --- a/tests/unit/Sessions.Tests.ps1 +++ b/tests/unit/Sessions.Tests.ps1 @@ -249,12 +249,12 @@ Describe 'Set-PodeSession' { Describe 'Remove-PodeSession' { It 'Throws an error if sessions are not configured' { - Mock Test-PodeSessionsConfigured { return $false } + Mock Test-PodeSessionsEnabled { return $false } { Remove-PodeSession } | Should Throw 'Sessions have not been configured' } It 'Does nothing if there is no session' { - Mock Test-PodeSessionsConfigured { return $true } + Mock Test-PodeSessionsEnabled { return $true } Mock Remove-PodeAuthSession {} $WebEvent = @{} @@ -264,7 +264,7 @@ Describe 'Remove-PodeSession' { } It 'Call removes the session' { - Mock Test-PodeSessionsConfigured { return $true } + Mock Test-PodeSessionsEnabled { return $true } Mock Remove-PodeAuthSession {} $WebEvent = @{ Session = @{} } @@ -276,18 +276,18 @@ Describe 'Remove-PodeSession' { Describe 'Save-PodeSession' { It 'Throws an error if sessions are not configured' { - Mock Test-PodeSessionsConfigured { return $false } + Mock Test-PodeSessionsEnabled { return $false } { Save-PodeSession } | Should Throw 'Sessions have not been configured' } It 'Throws error if there is no session' { - Mock Test-PodeSessionsConfigured { return $true } + Mock Test-PodeSessionsEnabled { return $true } $WebEvent = @{} { Save-PodeSession } | Should Throw 'There is no session available to save' } It 'Call saves the session' { - Mock Test-PodeSessionsConfigured { return $true } + Mock Test-PodeSessionsEnabled { return $true } Mock Invoke-PodeScriptBlock {} $WebEvent = @{ Session = @{ From c770db3d1ccb3a50401fbf0210bba1aca2b3de44 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Mon, 19 Feb 2024 22:45:37 +0000 Subject: [PATCH 21/84] #1241: Tab session docs, and session perf tweak --- docs/Tutorials/Middleware/Types/Sessions.md | 74 +++++++++++--- src/Private/Sessions.ps1 | 106 +++++++++----------- src/Public/Sessions.ps1 | 8 +- 3 files changed, 116 insertions(+), 72 deletions(-) diff --git a/docs/Tutorials/Middleware/Types/Sessions.md b/docs/Tutorials/Middleware/Types/Sessions.md index fc1c04e5d..f0fa5b939 100644 --- a/docs/Tutorials/Middleware/Types/Sessions.md +++ b/docs/Tutorials/Middleware/Types/Sessions.md @@ -2,23 +2,23 @@ Session Middleware is supported on web requests and responses in the form of signed a cookie/header and server-side data storage. When configured the middleware will check for a session cookie/header (usually called `pode.sid`) on the request; if a cookie/header is not found on the request, or the session is not in storage, then a new session is created and attached to the response. If there is a session, then the appropriate data for that session is loaded from storage. -The duration of the session cookie/header can be specified, as well as whether to extend the duration each time on each request. A secret-key to sign sessions can be supplied (default is a random GUID), as well as the ability to specify custom data stores - the default is in-memory, but custom storage could be anything like Redis/MongoDB/etc. +The duration of the session cookie/header can be specified, as well as whether to extend the duration each time on each request. A secret key to sign sessions can be supplied (default is a random GUID), as well as the ability to specify custom data stores - the default is in-memory, but custom storage could be anything like Redis/MongoDB/etc. !!! note Using sessions via headers is best used with REST APIs and the CLI. It's not advised to use them for normal websites, as browsers don't send back response headers in new requests - unlike cookies. !!! tip - Sessions are typically used in conjunction with Authentication, but can you use them standalone as well! + Sessions are typically used in conjunction with Authentication, but you can use them standalone as well! ## Usage -To initialise sessions in Pode you'll need to call [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware). This function will configure and automatically create the Middleware needed to enable sessions. By default sessions are set to use cookies, but support is also available for headers. +To initialise sessions in Pode you'll need to call [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware). This function will configure and automatically create the Middleware needed to enable sessions. By default, sessions are set to use cookies, but support is also available for headers. Sessions are automatically signed using a random GUID. For Pode running on a single server using the default in-memory storage this is OK, however if you're running Pode on multiple servers, or if you're defining a custom storage then a `-Secret` is required - this is so that sessions from different servers, or after a server restart, don't become corrupt and unusable. ### Cookies -The following is an example of how to setup session middleware using cookies. The duration of each session is defined as a total number of seconds via the `-Duration` parameter; here we set the duration to 120, so each session created will expire after 2mins, but the expiry time will be extended each time the session is used: +The following is an example of how to set up session middleware using cookies. The duration of each session is defined as a total number of seconds via the `-Duration` parameter; here we set the duration to 120, so each session created will expire after 2mins, but the expiry time will be extended each time the session is used: ```powershell Start-PodeServer { @@ -30,7 +30,7 @@ The default name of the session cookie is `pode.sid`, but this can be customised ### Headers -Sessions are also supported using headers - useful for CLI requests. The following example will enable sessions use headers instead of cookies, and will also set each session created to have a `-Duration` of 120 seconds: +Sessions are also supported using headers - useful for CLI requests. The following example will enable sessions to use headers instead of cookies, and will also set each session created to have a `-Duration` of 120 seconds: ```powershell Start-PodeServer { @@ -40,13 +40,54 @@ Start-PodeServer { When using headers, the default name of the session header in the request/response is `pode.sid` - this can be customised using the `-Name` parameter. When you make an initial request to authenticate some user, the `pode.sid` header will be returned in the response. You can then use the value of this header in subsequent requests for the authenticated user, and then make a call using the session one last time against some route to expire the session - or just let it automatically expire. +## Scope + +Sessions have two different Scopes: Browser and Tab. You can specify the scope using the `-Scope` parameter on [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware). + +!!! important + Using the Tabs scope requires additional frontend logic, and doesn't work out-of-the-box like the Browser scope. See more [below](#tabs). + +The default Scope is Browser, where any authentication and general session data is shared across all tabs in a browser. If you have a site with a view counter and you log in to your site on one tab, and then open another tab and navigate to your site, you'll be automatically logged in and see the same view counter results in both tabs. + +The Tabs scope still shares authentication data, so if you log in to your site in one tab and open it again in a different tab, you'll still be logged in. However, general data is separated; taking the view counter example above, both tabs will show different results for the view counter. + +### Tabs + +Unlike the Browser scope which works out-of-the-box when enabled, the Tabs scope requires additional frontend logic. + +For session data to be split across different tabs, you need to inform Pode about what the "TabId" is for each tab. This is done by supplying an `X-PODE-SESSION-TAB-ID` HTTP header in the request. From a browser on a normal page request, there's no way to supply this header, and the normal base session will be supplied instead - hence why authentication data remains shared across tabs. However, if you load the content of your site asynchronously, or via any other means, you can supply this header and it will let you split general session data across tabs. + +In websites, the TabId is typically generated via JavaScript, and stored in `window.sessionStorage`. However, you have to be careful with this approach, as it does make tab sessions susceptible to XSS attacks. The following is a similar approach as used by Pode.Web: + +```javascript +// set TabId +if (window.sessionStorage.TabId) { + window.TabId = window.sessionStorage.TabId; + window.sessionStorage.removeItem("TabId"); +} +else { + window.TabId = Math.floor(Math.random() * 1000000); +} + +// binding to persist TabId on refresh +window.addEventListener("beforeunload", function(e) { + window.sessionStorage.TabId = window.TabId; + return null; +}); +``` + +The TabId could then be sent as an HTTP header using AJAX. There are other approaches available online as well. + ## SessionIds -The inbuilt SessionId generator used for sessions is a GUID, but you can supply a custom generator using the `-Generator` parameter. +The built-in SessionId generator used for sessions is a GUID, but you can supply a custom generator using the `-Generator` parameter. If supplied, the `-Generator` is a scriptblock that must return a valid string. The string itself should be a random unique value, that can be used as a unique session identifier. -Within a route, or middleware, you can get the currently authenticated session'd ID using [`Get-PodeSessionId`](../../../../Functions/Sessions/Get-PodeSessionId). If there is no session, or the session is not authenticated, then `$null` is returned. This function can also returned the fully signed sessionId as well. If you want the sessionId even if it's not authenticated, then you can supply `-Force` to get the current SessionId back. +Within a route, or middleware, you can get the currently authenticated session's ID using [`Get-PodeSessionId`](../../../../Functions/Sessions/Get-PodeSessionId). If there is no session, or the session is not authenticated, then `$null` is returned. This function can also return the fully signed sessionId as well. If you want the sessionId even if it's not authenticated, then you can supply `-Force` to get the current SessionId back. + +!!! note + If you're using the Tab `-Scope`, then the SessionId will include the TabId as well, if one was supplied. ### Strict @@ -66,7 +107,7 @@ You can define a custom storage by supplying a `psobject` to the `-Storage` para [void] Delete([string] $sessionId) ``` -For example, the following is a mock up of a Storage for Redis (note that the functions are fake): +For example, the following is a mock-up of a Storage for Redis (note that the functions are fake): ```powershell # create the object @@ -97,7 +138,7 @@ Enable-PodeSessionMiddleware -Duration 120 -Storage $store -Secret 'schwifty' ## Session Data -To add data to a session you can utilise the `.Session.Data` property within the [web event](../../../WebEvent) object accessible in a Route - or other Middleware. The data will be saved to some storage at the end of the route automatically using Endware. When a request is made using the same SessionId, the data is loaded from the store. For example, incrementing some view counter: +To add data to a session you can use the `.Session.Data` property within the [web event](../../../WebEvent) object, which is accessible in a Route or other Middleware. The data will be saved to some storage at the end of the request automatically using Endware. When a request is made using the same SessionId, the data is loaded from the store. For example, incrementing some view counter: ```powershell Add-PodeRoute -Method Get -Path '/' -ScriptBlock { @@ -105,7 +146,7 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { } ``` -You can also use the `$session:` variable scope, which will get/set data on the current session for the name supplied. You can use `$session:` anywhere a `$WebEvent` is available - such as Routes, Middleware, Authentication and Endware. The same view counter example above would now be as follows: +You can also use the `$session:` variable scope, which will get/set data on the current session for the name supplied. You can use `$session:` anywhere a `$WebEvent` is available - such as Routes, Middleware, Authentication, and Endware. The same view counter example above would now be as follows: ```powershell Add-PodeRoute -Method Get -Path '/' -ScriptBlock { @@ -116,11 +157,14 @@ Add-PodeRoute -Method Get -Path '/' -ScriptBlock { A session's data will be automatically saved by Pode at the end of each request, but you can force the data of the current session to be saved by using [`Save-PodeSession`](../../../../Functions/Sessions/Save-PodeSession). !!! important - `$session:` can only be used in the main scriptblocks of Routes, etc. If you attempt to use it in a function of a custom module, it will fail; even if you're using the function in a route. Pode remaps `$session:` on server start, and can only do this to the main scriptblocks supplied to functions such as `Add-PodeRoute`. In these scenarios you will have to use `$WebEvent.Session.Data`. + `$session:` can only be used in the main scriptblocks of Routes, etc. If you attempt to use it in a function of a custom module, it will fail; even if you're using the function in a route. Pode remaps `$session:` on server start, and can only do this to the main scriptblocks supplied to functions such as `Add-PodeRoute`. In these scenarios, you will have to use `$WebEvent.Session.Data`. + +!!! note + If using the Tab `-Scope`, any session data will be stored separately from other tabs. This allows you to have multiple tabs open for the same site/page but all with separate session data. Any authentication data will still be shared. ## Expiry -When you enable Sessions using [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) you can define the duration of each session created, in seconds, using the `-Duration` parameter. When a session is created its expiry is set to `DateTime.UtcNow + Duration`, and by default a session will automatically expire when the calculated DateTime is reached: +When you enable Sessions using [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) you can define the duration of each session created, in seconds, using the `-Duration` parameter. When a session is created its expiry is set to `DateTime.UtcNow + Duration`, and by default, a session will automatically expire when the calculated DateTime is reached: ```powershell Start-PodeServer { @@ -128,7 +172,7 @@ Start-PodeServer { } ``` -You can tell Pode to reset/extend each session's expiry on each request sent, that uses that SessionId, by passing the `-Extend` switch. When a session's expiry is reset/extended, the DateTime/Duration calculation is re-calculated: +You can tell Pode to reset/extend each session's expiry on each request sent, that uses that SessionId, by supplying the `-Extend` switch. When a session's expiry is reset/extended, the DateTime/Duration calculation is re-calculated: ```powershell Start-PodeServer { @@ -138,7 +182,7 @@ Start-PodeServer { ### Retrieve -You can retrieve the expiry for the current session by using [`Get-PodeSessionExpiry`](../../../../Functions/Sessions/Get-PodeSessionExpiry). If you use this function without `-Extend` specified originally then this will return the explicit DateTime the current session will expire. However, if you did setup sessions to extend the this function will return the recalculated expiry for the current session on each call: +You can retrieve the expiry for the current session by using [`Get-PodeSessionExpiry`](../../../../Functions/Sessions/Get-PodeSessionExpiry). If you use this function without `-Extend` specified originally then this will return the explicit DateTime the current session will expire. However, if you did set up sessions to extend then this function will return the recalculated expiry for the current session on each call: ```powershell Start-PodeServer { @@ -168,7 +212,7 @@ Start-PodeServer { ### Reset -For any session created when `-Extend` wasn't supplied to [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) will always have a explicit DateTime set for expiring. However, you can reset this expiry date using [`Reset-PodeSessionExpiry`](../../../../Functions/Sessions/Reset-PodeSessionExpiry), and the current session's expiry will be recalculated from now plus the specifed `-Duration`: +For any session created when `-Extend` wasn't supplied to [`Enable-PodeSessionMiddleware`](../../../../Functions/Sessions/Enable-PodeSessionMiddleware) will always have an explicit DateTime set for expiring. However, you can reset this expiry date using [`Reset-PodeSessionExpiry`](../../../../Functions/Sessions/Reset-PodeSessionExpiry), and the current session's expiry will be recalculated from now plus the specified `-Duration`: ```powershell Start-PodeServer { diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1 index 32c8642dd..6c9a8b03d 100644 --- a/src/Private/Sessions.ps1 +++ b/src/Private/Sessions.ps1 @@ -120,7 +120,7 @@ function Get-PodeSession { } # generate the session data - $data = @{ + return @{ Name = $name Id = $sessionId TabId = $tabId @@ -129,8 +129,6 @@ function Get-PodeSession { TimeStamp = $null Data = @{} } - - return $data } function Revoke-PodeSession { @@ -145,10 +143,7 @@ function Revoke-PodeSession { } # remove session from store - $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Delete - - # blank the session - $WebEvent.Session.Clear() + Remove-PodeSessionInternal } function Set-PodeSessionDataHash { @@ -180,69 +175,69 @@ function Test-PodeSessionDataHash { return ($WebEvent.Session.DataHash -eq $hash) } -function Set-PodeSessionHelpers { - if ($null -eq $WebEvent.Session) { - throw 'No session available to set helpers' +function Save-PodeSessionInternal { + param( + [switch] + $Force + ) + + # do nothing if session has no ID + if ([string]::IsNullOrEmpty($WebEvent.Session.FullId)) { + return } - # force save a session's data to the store - $WebEvent.Session | Add-Member -MemberType NoteProperty -Name Save -Value { - param($check) + # only save if check and hashes different, but not if extending expiry or updated + if (!$WebEvent.Session.Extend -and $Force -and (Test-PodeSessionDataHash)) { + return + } - # do nothing if session has no ID - if ([string]::IsNullOrEmpty($WebEvent.Session.FullId)) { - return - } + # generate the expiry + $expiry = Get-PodeSessionExpiry - # only save if check and hashes different, but not if extending expiry or updated - if (!$WebEvent.Session.Extend -and $check -and (Test-PodeSessionDataHash)) { - return + # the data to save - which will be the data, and some extra metadata like timestamp + $data = @{ + Version = 3 + Metadata = @{ + TimeStamp = $WebEvent.Session.TimeStamp } + Data = $WebEvent.Session.Data + } - # generate the expiry - $expiry = Get-PodeSessionExpiry - - # the data to save - which will be the data, and some extra metadata like timestamp - $data = @{ + # save base session data to store + if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and $WebEvent.Session.TabId) { + $authData = @{ Version = 3 Metadata = @{ TimeStamp = $WebEvent.Session.TimeStamp + Tabbed = $true + } + Data = @{ + Auth = $WebEvent.Session.Data.Auth } - Data = $WebEvent.Session.Data } - # save base session data to store - if (!$PodeContext.Server.Sessions.Info.Scope.IsBrowser -and $WebEvent.Session.TabId) { - $authData = @{ - Version = 3 - Metadata = @{ - TimeStamp = $WebEvent.Session.TimeStamp - Tabbed = $true - } - Data = @{ - Auth = $WebEvent.Session.Data.Auth - } - } + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.Id, $authData, $expiry) -Splat + $data.Metadata['Parent'] = $WebEvent.Session.Id + } - $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.Id, $authData, $expiry) -Splat - $data.Metadata['Parent'] = $WebEvent.Session.Id - } + # save session data to store + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.FullId, $data, $expiry) -Splat - # save session data to store - $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Set -Arguments @($WebEvent.Session.FullId, $data, $expiry) -Splat + # update session's data hash + Set-PodeSessionDataHash +} - # update session's data hash - Set-PodeSessionDataHash +function Remove-PodeSessionInternal { + if ($null -eq $WebEvent.Session) { + return } - # delete the current session - $WebEvent.Session | Add-Member -MemberType NoteProperty -Name Delete -Value { - # remove data from store - $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $WebEvent.Session.Id + # remove data from store + $null = Invoke-PodeScriptBlock -ScriptBlock $PodeContext.Server.Sessions.Store.Delete -Arguments $WebEvent.Session.Id - # clear session - $WebEvent.Session.Clear() - } + # clear session + $WebEvent.Session.Clear() + $WebEvent.Session = $null } function Get-PodeSessionInMemStore { @@ -405,9 +400,6 @@ function Get-PodeSessionMiddleware { # set data hash Set-PodeSessionDataHash - # add helper methods to current session - Set-PodeSessionHelpers - # add session to response if it's new or extendible if ($new -or $WebEvent.Session.Extend) { Set-PodeSession @@ -416,7 +408,9 @@ function Get-PodeSessionMiddleware { # assign endware for session to set cookie/header $WebEvent.OnEnd += @{ Logic = { - Save-PodeSession -Force + if ($null -ne $WebEvent.Session) { + Save-PodeSession -Force + } } } } diff --git a/src/Public/Sessions.ps1 b/src/Public/Sessions.ps1 index 7d865695a..0e1107ff9 100644 --- a/src/Public/Sessions.ps1 +++ b/src/Public/Sessions.ps1 @@ -21,6 +21,12 @@ A custom ScriptBlock to generate a random unique SessionId. The value returned m .PARAMETER Storage A custom PSObject that defines methods for Delete, Get, and Set. This allow you to store Sessions in custom Storage such as Redis. A Secret is required. +.PARAMETER Scope +The Scope that the Session applies to, possible values are Browser and Tab (Default: Browser). +The Browser scope is the default logic, where authentication and general data for the sessions are shared across all tabs. +The Tab scope keep the authentication data shared across all tabs, but general data is separated across different tabs. +For the Tab scope, the "Tab ID" required will be sourced from the "X-PODE-SESSION-TAB-ID" header. + .PARAMETER Extend If supplied, the Sessions will have their durations extended on each successful Request. @@ -220,7 +226,7 @@ function Save-PodeSession { } # save the session - $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Session.Save -Arguments @($Force.IsPresent) -Splat + Save-PodeSessionInternal -Force:$Force } <# From 54bb76bdb85ab4d6032f3f21d6be517e2d688d5d Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Mon, 19 Feb 2024 23:07:35 +0000 Subject: [PATCH 22/84] #1241: test fix --- tests/unit/Sessions.Tests.ps1 | 57 ++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/tests/unit/Sessions.Tests.ps1 b/tests/unit/Sessions.Tests.ps1 index 8201792a0..266a634f5 100644 --- a/tests/unit/Sessions.Tests.ps1 +++ b/tests/unit/Sessions.Tests.ps1 @@ -37,11 +37,11 @@ Describe 'Get-PodeSession' { $PodeContext = @{ Server = @{ - Cookies = @{} + Cookies = @{} Sessions = @{ - Name = 'pode.sid' + Name = 'pode.sid' Secret = 'key' - Info = @{ 'Duration' = 60 } + Info = @{ 'Duration' = 60 } } } } @@ -54,16 +54,17 @@ Describe 'Get-PodeSession' { $cookie = [System.Net.Cookie]::new('pode.sid', 's:value.kPv88V5o2uJ29sqh2a7P/f3dxcg+JdZJZT3GTIE=') $WebEvent = @{ Cookies = @{ - 'pode.sid' = $cookie - } } + 'pode.sid' = $cookie + } + } $PodeContext = @{ Server = @{ - Cookies = @{} + Cookies = @{} Sessions = @{ - Name = 'pode.sid' + Name = 'pode.sid' Secret = 'key' - Info = @{ 'Duration' = 60 } + Info = @{ 'Duration' = 60 } } } } @@ -76,16 +77,17 @@ Describe 'Get-PodeSession' { $cookie = [System.Net.Cookie]::new('pode.sid', 's:value.kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=') $WebEvent = @{ Cookies = @{ - 'pode.sid' = $cookie - } } + 'pode.sid' = $cookie + } + } $PodeContext = @{ Server = @{ - Cookies = @{} + Cookies = @{} Sessions = @{ - Name = 'pode.sid' + Name = 'pode.sid' Secret = 'key' - Info = @{ 'Duration' = 60 } + Info = @{ 'Duration' = 60 } } } } @@ -146,11 +148,11 @@ Describe 'New-PodeSession' { $PodeContext = @{ Server = @{ - Cookies = @{} + Cookies = @{} Sessions = @{ - Name = 'pode.sid' - Secret = 'key' - Info = @{ 'Duration' = 60 } + Name = 'pode.sid' + Secret = 'key' + Info = @{ 'Duration' = 60 } GenerateId = {} } } @@ -165,7 +167,7 @@ Describe 'New-PodeSession' { $WebEvent.Session.Data.Count | Should Be 0 $crypto = [System.Security.Cryptography.SHA256]::Create() - $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data| ConvertTo-Json -Depth 10 -Compress))) + $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data | ConvertTo-Json -Depth 10 -Compress))) $hash = [System.Convert]::ToBase64String($hash) $WebEvent.Session.DataHash | Should Be $hash @@ -191,12 +193,12 @@ Describe 'Test-PodeSessionDataHash' { It 'Returns true for a valid hash' { $WebEvent = @{ Session = @{ - 'Data' = @{ 'Counter' = 2; }; + 'Data' = @{ 'Counter' = 2; } } } $crypto = [System.Security.Cryptography.SHA256]::Create() - $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data| ConvertTo-Json -Depth 10 -Compress))) + $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data | ConvertTo-Json -Depth 10 -Compress))) $hash = [System.Convert]::ToBase64String($hash) $WebEvent.Session.DataHash = $hash @@ -234,9 +236,9 @@ Describe 'Set-PodeSession' { $WebEvent = @{ Session = @{ - 'Name' = 'name'; - 'Id' = 'sessionId'; - 'Cookie' = @{}; + 'Name' = 'name' + 'Id' = 'sessionId' + 'Cookie' = @{} } } @@ -288,13 +290,14 @@ Describe 'Save-PodeSession' { It 'Call saves the session' { Mock Test-PodeSessionsEnabled { return $true } - Mock Invoke-PodeScriptBlock {} + Mock Save-PodeSessionInternal {} $WebEvent = @{ Session = @{ - Save = {} - } } + Save = {} + } + } Save-PodeSession - Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It + Assert-MockCalled Save-PodeSessionInternal -Times 1 -Scope It } } \ No newline at end of file From 41a19933f6f36bc3fa68f61cd138b1e6ce6a5a56 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Mon, 4 Mar 2024 23:41:19 +0000 Subject: [PATCH 23/84] #1245: initial work for SSE support --- .vscode/settings.json | 13 +- examples/public/scripts/sse.js | 45 +++++++ examples/sse.ps1 | 35 ++++++ examples/views/sse-home.html | 18 +++ src/Listener/PodeContext.cs | 20 ++- src/Listener/PodeListener.cs | 92 +++++++++++++- src/Listener/PodeResponse.cs | 217 ++++++++++++++++++++++---------- src/Listener/PodeServerEvent.cs | 25 ++++ src/Listener/PodeSignal.cs | 7 +- src/Listener/PodeSseScope.cs | 9 ++ src/Pode.psd1 | 4 + src/Private/Context.ps1 | 4 + src/Private/PodeServer.ps1 | 1 + src/Private/Server.ps1 | 1 + src/Public/Responses.ps1 | 134 ++++++++++++++++++++ 15 files changed, 551 insertions(+), 74 deletions(-) create mode 100644 examples/public/scripts/sse.js create mode 100644 examples/sse.ps1 create mode 100644 examples/views/sse-home.html create mode 100644 src/Listener/PodeServerEvent.cs create mode 100644 src/Listener/PodeSseScope.cs diff --git a/.vscode/settings.json b/.vscode/settings.json index 0eada4b5d..fb2be60f2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,5 +20,16 @@ "powershell.codeFormatting.whitespaceBeforeOpenParen": true, "powershell.codeFormatting.whitespaceBetweenParameters": false, "powershell.codeFormatting.whitespaceInsideBrace": true, - "files.trimTrailingWhitespace": true + "files.trimTrailingWhitespace": true, + "files.associations": { + "*.pode": "html" + }, + "[html]": { + "editor.formatOnSave": false + }, + "javascript.format.insertSpaceAfterCommaDelimiter": true, + "javascript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, + "javascript.format.insertSpaceAfterKeywordsInControlFlowStatements": true, + "javascript.format.insertSpaceBeforeAndAfterBinaryOperators": true, + "javascript.format.insertSpaceBeforeFunctionParenthesis": false } \ No newline at end of file diff --git a/examples/public/scripts/sse.js b/examples/public/scripts/sse.js new file mode 100644 index 000000000..b63ef935b --- /dev/null +++ b/examples/public/scripts/sse.js @@ -0,0 +1,45 @@ +$(document).ready(() => { + $('button#local').off('click').on('click', (e) => { + const sse = new EventSource("/data"); + + sse.addEventListener("Action", (e) => { + $('#messages').append(`

Action: ${e.data}

`); + }); + + sse.addEventListener("BoldOne", (e) => { + $('#messages').append(`

BoldOne: ${e.data}

`); + }); + + sse.addEventListener("pode.close", (e) => { + console.log('CLOSE'); + sse.close(); + }); + + sse.addEventListener("pode.open", (e) => { + var data = JSON.parse(e.data); + console.log(`OPEN: ${data.clientId}`); + }); + + sse.onerror = (e) => { + console.log('ERROR!'); + } + }); + + $('button#global').off('click').on('click', (e) => { + const sse2 = new EventSource("/sse"); + + sse2.onmessage = (e) => { + $('#messages').append(`

${e.data}

`); + }; + + sse2.onerror = (e) => { + console.log('ERROR2!'); + sse.close(); + }; + + sse2.addEventListener("pode.close", (e) => { + console.log('CLOSE'); + sse2.close(); + }); + }); +}) \ No newline at end of file diff --git a/examples/sse.ps1 b/examples/sse.ps1 new file mode 100644 index 000000000..5e155c7a2 --- /dev/null +++ b/examples/sse.ps1 @@ -0,0 +1,35 @@ +$path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) +Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop + +# or just: +# Import-Module Pode + +# create a server, and start listening on port 8085 +Start-PodeServer -Threads 3 { + # listen on localhost:8085 + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + + # log errors + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging -Levels * + + # open local sse connection, and send back data + Add-PodeRoute -Method Get -Path '/data' -ScriptBlock { + Set-PodeSseConnection -Name 'Data' -Scope Local + Send-PodeSseMessage -Id 1234 -EventType Action -Data 'hello, there!' + Start-Sleep -Seconds 3 + Send-PodeSseMessage -Id 1337 -EventType BoldOne -Data 'general kenobi' + } + + # home page to get sse events + Add-PodeRoute -Method Get -Path '/' -ScriptBlock { + Write-PodeViewResponse -Path 'sse-home' + } + + Add-PodeRoute -Method Get -Path '/sse' -ScriptBlock { + $clientId = Set-PodeSseConnection -Name 'Test' -Scope Global + } + + Add-PodeTimer -Name 'SendMessage' -Interval 10 -ScriptBlock { + Send-PodeSseMessage -Name 'Test' -Data "A Message! $(Get-Random -Minimum 1 -Maximum 100)" + } +} \ No newline at end of file diff --git a/examples/views/sse-home.html b/examples/views/sse-home.html new file mode 100644 index 000000000..7142e6e83 --- /dev/null +++ b/examples/views/sse-home.html @@ -0,0 +1,18 @@ + + + + SSE Home + + + + + + +

Example of using SSE

+

Messages streamed from a local SSE connection

+ + +
+ + + \ No newline at end of file diff --git a/src/Listener/PodeContext.cs b/src/Listener/PodeContext.cs index 3c1bf95fc..ce4ceb38c 100644 --- a/src/Listener/PodeContext.cs +++ b/src/Listener/PodeContext.cs @@ -79,7 +79,7 @@ public PodeSignalRequest SignalRequest public bool IsKeepAlive { - get => (Request.IsKeepAlive); + get => ((Request.IsKeepAlive && Response.SseScope != PodeSseScope.Local) || Response.SseScope == PodeSseScope.Global); } public bool IsErrored @@ -123,6 +123,11 @@ public PodeContext(Socket socket, PodeSocket podeSocket, PodeListener listener) private void TimeoutCallback(object state) { + if (Response.SseEnabled) + { + return; + } + ContextTimeoutToken.Cancel(); State = PodeContextState.Timeout; @@ -256,6 +261,11 @@ public void RenewTimeoutToken() ContextTimeoutToken = new CancellationTokenSource(); } + public void CancelTimeout() + { + TimeoutTimer.Dispose(); + } + public async void Receive() { try @@ -412,6 +422,12 @@ public void Dispose(bool force) if (!_awaitingBody && (!IsKeepAlive || force)) { State = PodeContextState.Closed; + + if (Response.SseEnabled) + { + Response.CloseSseConnection(); + } + Request.Dispose(); } @@ -423,7 +439,7 @@ public void Dispose(bool force) catch {} // if keep-alive, or awaiting body, setup for re-receive - if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout)) && !force) + if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force) { StartReceive(); } diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index 9ad432b0f..b9679ea4a 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -11,6 +11,7 @@ public class PodeListener : PodeConnector private IList Sockets; public IDictionary Signals { get; private set; } + public IDictionary> ServerEvents { get; private set; } public PodeItemQueue Contexts { get; private set; } public PodeItemQueue ServerSignals { get; private set; } public PodeItemQueue ClientSignals { get; private set; } @@ -50,6 +51,7 @@ public bool ShowServerDetails { Sockets = new List(); Signals = new Dictionary(); + ServerEvents = new Dictionary>(); Contexts = new PodeItemQueue(); ServerSignals = new PodeItemQueue(); @@ -110,6 +112,79 @@ public void AddSignal(PodeSignal signal) } } + public void AddSseConnection(PodeServerEvent sse) + { + lock (ServerEvents) + { + // add sse name + if (!ServerEvents.ContainsKey(sse.Name)) + { + ServerEvents.Add(sse.Name, new Dictionary()); + } + + // add sse connection + if (ServerEvents[sse.Name].ContainsKey(sse.ClientId)) + { + ServerEvents[sse.Name][sse.ClientId]?.Dispose(); + ServerEvents[sse.Name][sse.ClientId] = sse; + } + else + { + ServerEvents[sse.Name].Add(sse.ClientId, sse); + } + } + } + + public void SendSseMessage(string name, string[] clientIds, string eventType, string data, string id = null) + { + Task.Factory.StartNew(() => { + if (!ServerEvents.ContainsKey(name)) + { + return; + } + + if (clientIds == default(string[]) || clientIds.Length == 0) + { + clientIds = ServerEvents[name].Keys.ToArray(); + } + + foreach (var clientId in clientIds) + { + if (!ServerEvents[name].ContainsKey(clientId)) + { + continue; + } + + ServerEvents[name][clientId].Context.Response.SendSseMessage(eventType, data, id); + } + }, CancellationToken); + } + + public void CloseSseConnection(string name, string[] clientIds) + { + Task.Factory.StartNew(() => { + if (!ServerEvents.ContainsKey(name)) + { + return; + } + + if (clientIds == default(string[]) || clientIds.Length == 0) + { + clientIds = ServerEvents[name].Keys.ToArray(); + } + + foreach (var clientId in clientIds) + { + if (!ServerEvents[name].ContainsKey(clientId)) + { + continue; + } + + ServerEvents[name][clientId].Context.Response.CloseSseConnection(); + } + }, CancellationToken); + } + public PodeServerSignal GetServerSignal(CancellationToken cancellationToken = default(CancellationToken)) { return ServerSignals.Get(cancellationToken); @@ -187,11 +262,26 @@ protected override void Close() PodeHelpers.WriteErrorMessage($"Closing signals", this, PodeLoggingLevel.Verbose); foreach (var _signal in Signals.Values.ToArray()) { - _signal.Context.Dispose(true); + _signal.Dispose(); } Signals.Clear(); PodeHelpers.WriteErrorMessage($"Closed signals", this, PodeLoggingLevel.Verbose); + + // close connected server events + PodeHelpers.WriteErrorMessage($"Closing server events", this, PodeLoggingLevel.Verbose); + foreach (var _sseName in ServerEvents.Values.ToArray()) + { + foreach (var _sse in _sseName.Values.ToArray()) + { + _sse.Dispose(); + } + + _sseName.Clear(); + } + + ServerEvents.Clear(); + PodeHelpers.WriteErrorMessage($"Closed server events", this, PodeLoggingLevel.Verbose); } } } \ No newline at end of file diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index a56963c6b..be1c67b8e 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -14,12 +14,24 @@ public class PodeResponse : IDisposable public int StatusCode = 200; public bool SendChunked = false; public MemoryStream OutputStream { get; private set; } - public bool Sent { get; private set; } public bool IsDisposed { get; private set; } private PodeContext Context; private PodeRequest Request { get => Context.Request; } + public PodeSseScope SseScope { get; private set; } = PodeSseScope.None; + public bool SseEnabled + { + get => SseScope != PodeSseScope.None; + } + + public bool SentHeaders { get; private set; } + public bool SentBody { get; private set; } + public bool Sent + { + get => SentHeaders && SentBody; + } + private string _statusDesc = string.Empty; public string StatusDescription { @@ -73,43 +85,17 @@ public PodeResponse() public void Send() { - if (Sent || IsDisposed) + if (Sent || IsDisposed || (SentHeaders && SseEnabled)) { return; } PodeHelpers.WriteErrorMessage($"Sending response", Context.Listener, PodeLoggingLevel.Verbose, Context); - Sent = true; try { - // start building the response message - var message = HttpResponseLine; - - // default headers - SetDefaultHeaders(); - - // write the response headers - if (!Context.IsTimeout) - { - message += BuildHeaders(Headers); - } - - var buffer = Encoding.GetBytes(message); - - // stream response output - if (Request.InputStream.CanWrite) - { - Request.InputStream.WriteAsync(buffer, 0, buffer.Length).Wait(Context.Listener.CancellationToken); - - if (!Context.IsTimeout && OutputStream.Length > 0) - { - OutputStream.WriteTo(Request.InputStream); - } - } - - message = string.Empty; - buffer = default(byte[]); + SendHeaders(Context.IsTimeout); + SendBody(Context.IsTimeout); PodeHelpers.WriteErrorMessage($"Response sent", Context.Listener, PodeLoggingLevel.Verbose, Context); } catch (OperationCanceledException) {} @@ -125,42 +111,23 @@ public void Send() } finally { - Request.InputStream.Flush(); + Flush(); } } public void SendTimeout() { - if (Sent || IsDisposed) + if (SentHeaders || IsDisposed) { return; } PodeHelpers.WriteErrorMessage($"Sending response timed-out", Context.Listener, PodeLoggingLevel.Verbose, Context); - Sent = true; StatusCode = 408; try { - // start building the response message - var message = HttpResponseLine; - - // default headers - Headers.Clear(); - SetDefaultHeaders(); - - // write the response headers - message += BuildHeaders(Headers); - var buffer = Encoding.GetBytes(message); - - // stream response output - if (Request.InputStream.CanWrite) - { - Request.InputStream.WriteAsync(buffer, 0, buffer.Length).Wait(Context.Listener.CancellationToken); - } - - message = string.Empty; - buffer = default(byte[]); + SendHeaders(true); PodeHelpers.WriteErrorMessage($"Response timed-out sent", Context.Listener, PodeLoggingLevel.Verbose, Context); } catch (OperationCanceledException) {} @@ -175,11 +142,129 @@ public void SendTimeout() throw; } finally + { + Flush(); + } + } + + private void SendHeaders(bool timeout) + { + if (SentHeaders || !Request.InputStream.CanWrite) + { + return; + } + + // default headers + if (timeout) + { + Headers.Clear(); + } + + SetDefaultHeaders(); + + // stream response output + var buffer = Encoding.GetBytes(BuildHeaders(Headers)); + Request.InputStream.WriteAsync(buffer, 0, buffer.Length).Wait(Context.Listener.CancellationToken); + buffer = default(byte[]); + SentHeaders = true; + } + + private void SendBody(bool timeout) + { + if (SentBody || SseEnabled || !Request.InputStream.CanWrite) + { + return; + } + + // stream response output + if (!timeout && OutputStream.Length > 0) + { + OutputStream.WriteTo(Request.InputStream); + } + + SentBody = true; + } + + public void Flush() + { + if (Request.InputStream.CanWrite) { Request.InputStream.Flush(); } } + public string SetSseConnection(PodeSseScope scope, string name, int retry, bool allowAllOrigins) + { + // do nothing for no scope + if (scope == PodeSseScope.None) + { + return null; + } + + // cancel timeout + Context.CancelTimeout(); + SseScope = scope; + + // set appropriate SSE headers + Headers.Clear(); + ContentType = "text/event-stream"; + Headers.Add("Cache-Control", "no-cache"); + Headers.Add("Connection", "keep-alive"); + + if (allowAllOrigins) + { + Headers.Add("Access-Control-Allow-Origin", "*"); + } + + // generate clientId + var clientId = PodeHelpers.NewGuid(); + Headers.Set("X-Pode-ClientId", clientId); + + // send headers, and open event + Send(); + SendSseRetry(retry); + SendSseMessage("pode.open", $"{{\"clientId\": \"{clientId}\"}}"); + + // if global, cache connection in listener + if (scope == PodeSseScope.Global) + { + Context.Listener.AddSseConnection(new PodeServerEvent(Context, name, clientId)); + } + + // return clientId + return clientId; + } + + public void CloseSseConnection() + { + SendSseMessage("pode.close", string.Empty); + } + + public void SendSseMessage(string eventType, string data, string id = null) + { + if (!string.IsNullOrEmpty(id)) + { + WriteLine($"id: {id}"); + } + + if (!string.IsNullOrEmpty(eventType)) + { + WriteLine($"event: {eventType}"); + } + + WriteLine($"data: {data}{PodeHelpers.NEW_LINE}", true); + } + + public void SendSseRetry(int retry) + { + if (retry <= 0) + { + return; + } + + WriteLine($"retry: {retry}", true); + } + public void SendSignal(PodeServerSignal signal) { if (!string.IsNullOrEmpty(signal.Value)) @@ -242,13 +327,12 @@ public void WriteFrame(string message, PodeWsOpCode opCode = PodeWsOpCode.Text, public void WriteLine(string message, bool flush = false) { - var msgBytes = Encoding.GetBytes($"{message}{PodeHelpers.NEW_LINE}"); - Write(msgBytes, flush); + Write(Encoding.GetBytes($"{message}{PodeHelpers.NEW_LINE}"), flush); } public void Write(byte[] buffer, bool flush = false) { - if (IsDisposed) + if (Request.IsDisposed || !Request.InputStream.CanWrite) { return; } @@ -259,7 +343,7 @@ public void Write(byte[] buffer, bool flush = false) if (flush) { - Request.InputStream.Flush(); + Flush(); } } catch (OperationCanceledException) {} @@ -278,7 +362,7 @@ public void Write(byte[] buffer, bool flush = false) private void SetDefaultHeaders() { // ensure content length (remove for 1xx responses, ensure added otherwise) - if (StatusCode < 200) + if (StatusCode < 200 || SseEnabled) { Headers.Remove("Content-Length"); } @@ -323,7 +407,7 @@ private void SetDefaultHeaders() Headers.Add("X-Pode-ContextId", Context.ID); // close the connection, only if request didn't specify keep-alive - if (!Context.IsKeepAlive && !Context.IsWebSocket) + if (!Context.IsKeepAlive && !Context.IsWebSocket && !SseEnabled) { if (Headers.ContainsKey("Connection")) { @@ -336,24 +420,19 @@ private void SetDefaultHeaders() private string BuildHeaders(PodeResponseHeaders headers) { - if (headers.Count == 0) - { - return PodeHelpers.NEW_LINE; - } - - var str = string.Empty; + var builder = new StringBuilder(); + builder.Append(HttpResponseLine); foreach (var key in headers.Keys) { - var values = headers.Get(key); - foreach (var value in values) + foreach (var value in headers.Get(key)) { - str += $"{key}: {value}{PodeHelpers.NEW_LINE}"; + builder.Append($"{key}: {value}{PodeHelpers.NEW_LINE}"); } } - str += PodeHelpers.NEW_LINE; - return str; + builder.Append(PodeHelpers.NEW_LINE); + return builder.ToString(); } public void SetContext(PodeContext context) diff --git a/src/Listener/PodeServerEvent.cs b/src/Listener/PodeServerEvent.cs new file mode 100644 index 000000000..34b8e42ec --- /dev/null +++ b/src/Listener/PodeServerEvent.cs @@ -0,0 +1,25 @@ +using System; + +namespace Pode +{ + public class PodeServerEvent : IDisposable + { + public PodeContext Context { get; private set; } + public string Name { get; private set; } + public string ClientId { get; private set; } + public DateTime Timestamp { get; private set; } + + public PodeServerEvent(PodeContext context, string name, string clientId) + { + Context = context; + Name = name; + ClientId = clientId; + Timestamp = DateTime.UtcNow; + } + + public void Dispose() + { + Context.Dispose(true); + } + } +} \ No newline at end of file diff --git a/src/Listener/PodeSignal.cs b/src/Listener/PodeSignal.cs index 369e57547..6f653ac35 100644 --- a/src/Listener/PodeSignal.cs +++ b/src/Listener/PodeSignal.cs @@ -2,7 +2,7 @@ namespace Pode { - public class PodeSignal + public class PodeSignal : IDisposable { public PodeContext Context { get; private set; } public string Path { get; private set; } @@ -16,5 +16,10 @@ public PodeSignal(PodeContext context, string path, string clientId) ClientId = clientId; Timestamp = DateTime.UtcNow; } + + public void Dispose() + { + Context.Dispose(true); + } } } \ No newline at end of file diff --git a/src/Listener/PodeSseScope.cs b/src/Listener/PodeSseScope.cs new file mode 100644 index 000000000..8e57cb611 --- /dev/null +++ b/src/Listener/PodeSseScope.cs @@ -0,0 +1,9 @@ +namespace Pode +{ + public enum PodeSseScope + { + None, + Local, + Global + } +} \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index f722c1c92..908efd031 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -88,6 +88,10 @@ 'Use-PodePartialView', 'Send-PodeSignal', 'Add-PodeViewFolder', + 'Set-PodeSseConnection', + 'Send-PodeSseMessage', + 'Close-PodeSseConnection', + 'Send-PodeResponse', # utility helpers 'Close-PodeDisposable', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index bb008c024..f680f243c 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -151,6 +151,10 @@ function New-PodeContext { Listener = $null } + $ctx.Server.Http = @{ + Listener = $null + } + $ctx.Server.WebSockets = @{ Enabled = ($EnablePool -icontains 'websockets') Receiver = $null diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 2986aaa25..905c11cf1 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -85,6 +85,7 @@ function Start-PodeWebServer { $PodeContext.Listeners += $listener $PodeContext.Server.Signals.Enabled = $true $PodeContext.Server.Signals.Listener = $listener + $PodeContext.Server.Http.Listener = $listener } catch { $_ | Write-PodeErrorLog diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index d2408a1f0..709ad2051 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -226,6 +226,7 @@ function Restart-PodeInternalServer { # clear the sockets $PodeContext.Server.Signals.Enabled = $false $PodeContext.Server.Signals.Listener = $null + $PodeContext.Server.Http.Listener = $null $PodeContext.Listeners = @() $PodeContext.Receivers = @() $PodeContext.Watchers = @() diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index 3af31830c..2c1125e68 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -1580,4 +1580,138 @@ function Add-PodeViewFolder { # add the route(s) Write-Verbose "Adding View Folder: [$($Name)] $($Source)" $PodeContext.Server.Views[$Name] = $Source +} + +function Set-PodeSseConnection { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [ValidateSet('Local', 'Global')] + [string] + $Scope = 'Local', + + [Parameter()] + [int] + $RetryDuration = 0, + + [switch] + $AllowAllOrigins, + + [switch] + $Force + ) + + # check Accept header - unless forcing + if (!$Force -and ((Get-PodeHeader -Name 'Accept') -ine 'text/event-stream')) { + throw 'SSE can only be configured on requests with an Accept header value of text/event-stream' + } + + # set adn send SSE headers + $clientId = $WebEvent.Response.SetSseConnection($Scope, $Name, $RetryDuration, $AllowAllOrigins.IsPresent) + + # create SSE property on WebEvent + $WebEvent.Sse = @{ + Name = $Name + ClientId = $clientId + LastEventId = Get-PodeHeader -Name 'Last-Event-ID' + IsLocal = ($Scope -ieq 'local') + } +} + +function Send-PodeSseMessage { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Name, + + [Parameter()] + [string[]] + $ClientId, + + [Parameter()] + [string] + $Id, + + [Parameter()] + [string] + $EventType, + + [Parameter(Mandatory = $true)] + $Data, + + [Parameter()] + [int] + $Depth = 10 + ) + + # do nothing if no value + if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { + return + } + + # mode + $direct = $false + + # check WebEvent + if ([string]::IsNullOrEmpty($Name) -and ($null -ne $WebEvent.Sse)) { + $Name = $WebEvent.Sse + + if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) { + $ClientId = $WebEvent.Sse.ClientId + $direct = $true + } + } + + # error if no name + if ([string]::IsNullOrEmpty($Name)) { + throw 'An SSE connection name is required, either from -Name or $WebEvent.Sse.Name' + } + + # jsonify the value + if ($Data -isnot [string]) { + if ($Depth -le 0) { + $Data = (ConvertTo-Json -InputObject $Data -Compress) + } + else { + $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + } + } + + if ($direct) { + $WebEvent.Response.SendSseMessage($EventType, $Data, $Id) + } + else { + $PodeContext.Server.Http.Listener.SendSseMessage($Name, $ClientId, $EventType, $Data, $Id) + } +} + +function Close-PodeSseConnection { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string[]] + $ClientId + ) + + $PodeContext.Server.Http.Listener.CloseSseConnection($Name, $ClientId) +} + +#TODO: flag that this is a dangerous function, which will force send a response before the end of a route +# only use if you know what you're doing! +function Send-PodeResponse { + [CmdletBinding()] + param() + + if ($null -ne $WebEvent.Response) { + $WebEvent.Response.Send() + } } \ No newline at end of file From c05d911b71ce7ec0e5899fdc852dee6f63ba7a1b Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 17 Mar 2024 16:08:37 +0000 Subject: [PATCH 24/84] #1245: Add SSE ClientId signing, and verifying if passed. Also adds a base secret which is used for signing --- examples/sse.ps1 | 10 +- src/Listener/PodeHttpRequest.cs | 14 + src/Listener/PodeListener.cs | 4 +- src/Listener/PodeResponse.cs | 17 +- src/Pode.psd1 | 12 +- src/Private/Context.ps1 | 9 + src/Private/Cryptography.ps1 | 66 ++++- src/Private/PodeServer.ps1 | 15 + src/Private/Sessions.ps1 | 19 +- src/Public/Cookies.ps1 | 29 +- src/Public/Headers.ps1 | 47 ++-- src/Public/Responses.ps1 | 123 --------- src/Public/SSE.ps1 | 471 ++++++++++++++++++++++++++++++++ src/Public/Sessions.ps1 | 11 +- 14 files changed, 653 insertions(+), 194 deletions(-) create mode 100644 src/Public/SSE.ps1 diff --git a/examples/sse.ps1 b/examples/sse.ps1 index 5e155c7a2..8e6d26c88 100644 --- a/examples/sse.ps1 +++ b/examples/sse.ps1 @@ -15,9 +15,9 @@ Start-PodeServer -Threads 3 { # open local sse connection, and send back data Add-PodeRoute -Method Get -Path '/data' -ScriptBlock { Set-PodeSseConnection -Name 'Data' -Scope Local - Send-PodeSseMessage -Id 1234 -EventType Action -Data 'hello, there!' + Send-PodeSseEvent -Id 1234 -EventType Action -Data 'hello, there!' Start-Sleep -Seconds 3 - Send-PodeSseMessage -Id 1337 -EventType BoldOne -Data 'general kenobi' + Send-PodeSseEvent -Id 1337 -EventType BoldOne -Data 'general kenobi' } # home page to get sse events @@ -26,10 +26,10 @@ Start-PodeServer -Threads 3 { } Add-PodeRoute -Method Get -Path '/sse' -ScriptBlock { - $clientId = Set-PodeSseConnection -Name 'Test' -Scope Global + Set-PodeSseConnection -Name 'Test' } - Add-PodeTimer -Name 'SendMessage' -Interval 10 -ScriptBlock { - Send-PodeSseMessage -Name 'Test' -Data "A Message! $(Get-Random -Minimum 1 -Maximum 100)" + Add-PodeTimer -Name 'SendEvent' -Interval 10 -ScriptBlock { + Send-PodeSseEvent -Name 'Test' -Data "An Event! $(Get-Random -Minimum 1 -Maximum 100)" } } \ No newline at end of file diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs index a78fcdb79..bf9c43958 100644 --- a/src/Listener/PodeHttpRequest.cs +++ b/src/Listener/PodeHttpRequest.cs @@ -34,6 +34,13 @@ public class PodeHttpRequest : PodeRequest private bool IsRequestLineValid; private MemoryStream BodyStream; + public string SseClientId { get; private set; } + public string SseClientName { get; private set; } + public bool HasSseClientId + { + get => !string.IsNullOrEmpty(SseClientId); + } + private string _body = string.Empty; public string Body { @@ -292,6 +299,13 @@ private int ParseHeaders(string[] reqLines, string newline) Type = PodeProtocolType.Ws; } + // do we have an SSE ClientId? + SseClientId = $"{Headers["X-Pode-Sse-Client-Id"]}"; + if (HasSseClientId) + { + SseClientName = $"{Headers["X-Pode-Sse-Client-Name"]}"; + } + // keep-alive? IsKeepAlive = (IsWebSocket || (Headers.ContainsKey("Connection") diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index b9679ea4a..382028370 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -135,7 +135,7 @@ public void AddSseConnection(PodeServerEvent sse) } } - public void SendSseMessage(string name, string[] clientIds, string eventType, string data, string id = null) + public void SendSseEvent(string name, string[] clientIds, string eventType, string data, string id = null) { Task.Factory.StartNew(() => { if (!ServerEvents.ContainsKey(name)) @@ -155,7 +155,7 @@ public void SendSseMessage(string name, string[] clientIds, string eventType, st continue; } - ServerEvents[name][clientId].Context.Response.SendSseMessage(eventType, data, id); + ServerEvents[name][clientId].Context.Response.SendSseEvent(eventType, data, id); } }, CancellationToken); } diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index be1c67b8e..1842943aa 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -193,7 +193,7 @@ public void Flush() } } - public string SetSseConnection(PodeSseScope scope, string name, int retry, bool allowAllOrigins) + public string SetSseConnection(PodeSseScope scope, string clientId, string name, int retry, bool allowAllOrigins) { // do nothing for no scope if (scope == PodeSseScope.None) @@ -217,13 +217,18 @@ public string SetSseConnection(PodeSseScope scope, string name, int retry, bool } // generate clientId - var clientId = PodeHelpers.NewGuid(); - Headers.Set("X-Pode-ClientId", clientId); + if (string.IsNullOrEmpty(clientId)) + { + clientId = PodeHelpers.NewGuid(); + } + + Headers.Set("X-Pode-Sse-Client-Id", clientId); + Headers.Set("X-Pode-Sse-Client-Name", name); // send headers, and open event Send(); SendSseRetry(retry); - SendSseMessage("pode.open", $"{{\"clientId\": \"{clientId}\"}}"); + SendSseEvent("pode.open", $"{{\"clientId\":\"{clientId}\",\"name\":\"{name}\"}}"); // if global, cache connection in listener if (scope == PodeSseScope.Global) @@ -237,10 +242,10 @@ public string SetSseConnection(PodeSseScope scope, string name, int retry, bool public void CloseSseConnection() { - SendSseMessage("pode.close", string.Empty); + SendSseEvent("pode.close", string.Empty); } - public void SendSseMessage(string eventType, string data, string id = null) + public void SendSseEvent(string eventType, string data, string id = null) { if (!string.IsNullOrEmpty(id)) { diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 908efd031..253884262 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -88,10 +88,18 @@ 'Use-PodePartialView', 'Send-PodeSignal', 'Add-PodeViewFolder', + 'Send-PodeResponse', + + # sse 'Set-PodeSseConnection', - 'Send-PodeSseMessage', + 'Send-PodeSseEvent', 'Close-PodeSseConnection', - 'Send-PodeResponse', + 'Test-PodeSseClientIdSigned', + 'Test-PodeSseClientIdValid', + 'Get-PodeSseClientId', + 'New-PodeSseClientId', + 'Enable-PodeSseSigning', + 'Disable-PodeSseSigning', # utility helpers 'Close-PodeDisposable', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index f680f243c..73c31c40d 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -97,6 +97,9 @@ function New-PodeContext { $ctx.Receivers = @() $ctx.Watchers = @() + # base secret that can used when needed, and a secret isn't supplied + $ctx.Server.BaseSecret = New-PodeGuid -Secure + # list of timers/schedules/tasks/fim $ctx.Timers = @{ Enabled = ($EnablePool -icontains 'timers') @@ -155,6 +158,12 @@ function New-PodeContext { Listener = $null } + $ctx.Server.Sse = @{ + Signed = $false + Secret = $null + Strict = $false + } + $ctx.Server.WebSockets = @{ Enabled = ($EnablePool -icontains 'websockets') Receiver = $null diff --git a/src/Private/Cryptography.ps1 b/src/Private/Cryptography.ps1 index 99a90e34c..8aa8faeb8 100644 --- a/src/Private/Cryptography.ps1 +++ b/src/Private/Cryptography.ps1 @@ -196,12 +196,22 @@ function Invoke-PodeValueSign { [string] $Value, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] + [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) + if ([string]::IsNullOrEmpty($Secret)) { + $Secret = $PodeContext.Server.BaseSecret + } + + if ($Strict) { + $Secret = ConvertTo-PodeStrictSecret -Secret $Secret + } + return "s:$($Value).$(Invoke-PodeHMACSHA256Hash -Value $Value -Secret $Secret)" } @@ -212,10 +222,12 @@ function Invoke-PodeValueUnsign { [string] $Value, - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] + [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) # the signed value must start with "s:" @@ -223,13 +235,21 @@ function Invoke-PodeValueUnsign { return $null } - # the signed value mised contain a dot - splitting value and signature + # the signed value must contain a dot - splitting value and signature $Value = $Value.Substring(2) $periodIndex = $Value.LastIndexOf('.') if ($periodIndex -eq -1) { return $null } + if ([string]::IsNullOrEmpty($Secret)) { + $Secret = $PodeContext.Server.BaseSecret + } + + if ($Strict) { + $Secret = ConvertTo-PodeStrictSecret -Secret $Secret + } + # get the raw value and signature $raw = $Value.Substring(0, $periodIndex) $sig = $Value.Substring($periodIndex + 1) @@ -241,6 +261,38 @@ function Invoke-PodeValueUnsign { return $raw } +function Test-PodeValueSigned { + param( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [string] + $Value, + + [Parameter()] + [string] + $Secret, + + [switch] + $Strict + ) + + if ([string]::IsNullOrEmpty($Value)) { + return $false + } + + $result = Invoke-PodeValueUnsign -Value $Value -Secret $Secret -Strict:$Strict + return ![string]::IsNullOrEmpty($result) +} + +function ConvertTo-PodeStrictSecret { + param( + [Parameter(Mandatory = $true)] + [string] + $Secret + ) + + return "$($Secret);$($WebEvent.Request.UserAgent);$($WebEvent.Request.RemoteEndPoint.Address.IPAddressToString)" +} + function New-PodeJwtSignature { param( [Parameter(Mandatory = $true)] diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 905c11cf1..e2deb0dc1 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -146,6 +146,7 @@ function Start-PodeWebServer { TransferEncoding = $null AcceptEncoding = $null Ranges = $null + Sse = $null } # if iis, and we have an app path, alter it @@ -172,6 +173,20 @@ function Start-PodeWebServer { throw $Request.Error } + # if we have an sse clientId, verify it and then set details in WebEvent + if ($WebEvent.Request.HasSseClientId) { + if (!(Test-PodeSseClientIdValid)) { + throw [System.Net.Http.HttpRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)") + } + + $WebEvent.Sse = @{ + Name = $WebEvent.Request.SseClientName + ClientId = $WebEvent.Request.SseClientId + LastEventId = $null + IsLocal = $false + } + } + # invoke global and route middleware if ((Invoke-PodeMiddleware -Middleware $PodeContext.Server.Middleware -Route $WebEvent.Path)) { # has the request been aborted diff --git a/src/Private/Sessions.ps1 b/src/Private/Sessions.ps1 index 6c9a8b03d..35445d1cc 100644 --- a/src/Private/Sessions.ps1 +++ b/src/Private/Sessions.ps1 @@ -38,30 +38,18 @@ function Get-PodeSessionFullId { return $SessionId } -function ConvertTo-PodeSessionStrictSecret { - param( - [Parameter(Mandatory = $true)] - [string] - $Secret - ) - - return "$($Secret);$($WebEvent.Request.UserAgent);$($WebEvent.Request.RemoteEndPoint.Address.IPAddressToString)" -} - function Set-PodeSession { if ($null -eq $WebEvent.Session) { throw 'there is no session available to set on the response' } # convert secret to strict mode + $strict = $PodeContext.Server.Sessions.Info.Strict $secret = $PodeContext.Server.Sessions.Secret - if ($PodeContext.Server.Sessions.Info.Strict) { - $secret = ConvertTo-PodeSessionStrictSecret -Secret $secret - } # set session on header if ($PodeContext.Server.Sessions.Info.UseHeaders) { - Set-PodeHeader -Name $WebEvent.Session.Name -Value $WebEvent.Session.Id -Secret $secret + Set-PodeHeader -Name $WebEvent.Session.Name -Value $WebEvent.Session.Id -Secret $secret -Strict:$strict } # set session as cookie @@ -70,6 +58,7 @@ function Set-PodeSession { -Name $WebEvent.Session.Name ` -Value $WebEvent.Session.Id ` -Secret $secret ` + -Strict:$strict ` -ExpiryDate (Get-PodeSessionExpiry) ` -HttpOnly:$PodeContext.Server.Sessions.Info.HttpOnly ` -Secure:$PodeContext.Server.Sessions.Info.Secure @@ -84,7 +73,7 @@ function Get-PodeSession { # convert secret to strict mode if ($PodeContext.Server.Sessions.Info.Strict) { - $secret = ConvertTo-PodeSessionStrictSecret -Secret $secret + $secret = ConvertTo-PodeStrictSecret -Secret $secret } # session from header diff --git a/src/Public/Cookies.ps1 b/src/Public/Cookies.ps1 index d319d442a..d8a951dc6 100644 --- a/src/Public/Cookies.ps1 +++ b/src/Public/Cookies.ps1 @@ -69,12 +69,15 @@ function Set-PodeCookie { $Discard, [switch] - $Secure + $Secure, + + [switch] + $Strict ) # sign the value if we have a secret if (![string]::IsNullOrWhiteSpace($Secret)) { - $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret) + $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict) } # create a new cookie @@ -135,6 +138,9 @@ function Get-PodeCookie { [string] $Secret, + [switch] + $Strict, + [switch] $Raw ) @@ -151,7 +157,7 @@ function Get-PodeCookie { # if a secret was supplied, attempt to unsign the cookie if (![string]::IsNullOrWhiteSpace($Secret)) { - $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret) + $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret -Strict:$Strict) if (![string]::IsNullOrWhiteSpace($value)) { $cookie.Value = $value } @@ -188,10 +194,13 @@ function Get-PodeCookieValue { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) - $cookie = Get-PodeCookie -Name $Name -Secret $Secret + $cookie = Get-PodeCookie -Name $Name -Secret $Secret -Strict:$Strict if ($null -eq $cookie) { return $null } @@ -288,16 +297,18 @@ function Test-PodeCookieSigned { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) $cookie = $WebEvent.Cookies[$Name] - if (($null -eq $cookie) -or [string]::IsNullOrWhiteSpace($cookie.Value)) { + if ($null -eq $cookie) { return $false } - $value = (Invoke-PodeValueUnsign -Value $cookie.Value -Secret $Secret) - return (![string]::IsNullOrWhiteSpace($value)) + return Test-PodeValueSigned -Value $cookie.Value -Secret $Secret -Strict:$Strict } <# diff --git a/src/Public/Headers.ps1 b/src/Public/Headers.ps1 index 223eeb8b9..b52a638c8 100644 --- a/src/Public/Headers.ps1 +++ b/src/Public/Headers.ps1 @@ -30,12 +30,15 @@ function Add-PodeHeader { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) # sign the value if we have a secret if (![string]::IsNullOrWhiteSpace($Secret)) { - $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret) + $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict) } # add the header to the response @@ -72,7 +75,10 @@ function Add-PodeHeaderBulk { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) foreach ($key in $Values.Keys) { @@ -80,7 +86,7 @@ function Add-PodeHeaderBulk { # sign the value if we have a secret if (![string]::IsNullOrWhiteSpace($Secret)) { - $value = (Invoke-PodeValueSign -Value $value -Secret $Secret) + $value = (Invoke-PodeValueSign -Value $value -Secret $Secret -Strict:$Strict) } # add the header to the response @@ -145,7 +151,10 @@ function Get-PodeHeader { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) # get the value for the header from the request @@ -153,7 +162,7 @@ function Get-PodeHeader { # if a secret was supplied, attempt to unsign the header's value if (![string]::IsNullOrWhiteSpace($Secret)) { - $header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret) + $header = (Invoke-PodeValueUnsign -Value $header -Secret $Secret -Strict:$Strict) } return $header @@ -191,12 +200,15 @@ function Set-PodeHeader { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) # sign the value if we have a secret if (![string]::IsNullOrWhiteSpace($Secret)) { - $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret) + $Value = (Invoke-PodeValueSign -Value $Value -Secret $Secret -Strict:$Strict) } # set the header on the response @@ -233,7 +245,10 @@ function Set-PodeHeaderBulk { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) foreach ($key in $Values.Keys) { @@ -241,7 +256,7 @@ function Set-PodeHeaderBulk { # sign the value if we have a secret if (![string]::IsNullOrWhiteSpace($Secret)) { - $value = (Invoke-PodeValueSign -Value $value -Secret $Secret) + $value = (Invoke-PodeValueSign -Value $value -Secret $Secret -Strict:$Strict) } # set the header on the response @@ -280,14 +295,12 @@ function Test-PodeHeaderSigned { [Parameter()] [string] - $Secret + $Secret, + + [switch] + $Strict ) $header = Get-PodeHeader -Name $Name - if ([string]::IsNullOrWhiteSpace($header)) { - return $false - } - - $value = (Invoke-PodeValueUnsign -Value $header -Secret $Secret) - return (![string]::IsNullOrWhiteSpace($value)) + return Test-PodeValueSigned -Value $header -Secret $Secret -Strict:$Strict } \ No newline at end of file diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index 2c1125e68..db5fae517 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -1582,129 +1582,6 @@ function Add-PodeViewFolder { $PodeContext.Server.Views[$Name] = $Source } -function Set-PodeSseConnection { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter()] - [ValidateSet('Local', 'Global')] - [string] - $Scope = 'Local', - - [Parameter()] - [int] - $RetryDuration = 0, - - [switch] - $AllowAllOrigins, - - [switch] - $Force - ) - - # check Accept header - unless forcing - if (!$Force -and ((Get-PodeHeader -Name 'Accept') -ine 'text/event-stream')) { - throw 'SSE can only be configured on requests with an Accept header value of text/event-stream' - } - - # set adn send SSE headers - $clientId = $WebEvent.Response.SetSseConnection($Scope, $Name, $RetryDuration, $AllowAllOrigins.IsPresent) - - # create SSE property on WebEvent - $WebEvent.Sse = @{ - Name = $Name - ClientId = $clientId - LastEventId = Get-PodeHeader -Name 'Last-Event-ID' - IsLocal = ($Scope -ieq 'local') - } -} - -function Send-PodeSseMessage { - [CmdletBinding()] - param( - [Parameter()] - [string] - $Name, - - [Parameter()] - [string[]] - $ClientId, - - [Parameter()] - [string] - $Id, - - [Parameter()] - [string] - $EventType, - - [Parameter(Mandatory = $true)] - $Data, - - [Parameter()] - [int] - $Depth = 10 - ) - - # do nothing if no value - if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { - return - } - - # mode - $direct = $false - - # check WebEvent - if ([string]::IsNullOrEmpty($Name) -and ($null -ne $WebEvent.Sse)) { - $Name = $WebEvent.Sse - - if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) { - $ClientId = $WebEvent.Sse.ClientId - $direct = $true - } - } - - # error if no name - if ([string]::IsNullOrEmpty($Name)) { - throw 'An SSE connection name is required, either from -Name or $WebEvent.Sse.Name' - } - - # jsonify the value - if ($Data -isnot [string]) { - if ($Depth -le 0) { - $Data = (ConvertTo-Json -InputObject $Data -Compress) - } - else { - $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) - } - } - - if ($direct) { - $WebEvent.Response.SendSseMessage($EventType, $Data, $Id) - } - else { - $PodeContext.Server.Http.Listener.SendSseMessage($Name, $ClientId, $EventType, $Data, $Id) - } -} - -function Close-PodeSseConnection { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string] - $Name, - - [Parameter()] - [string[]] - $ClientId - ) - - $PodeContext.Server.Http.Listener.CloseSseConnection($Name, $ClientId) -} - #TODO: flag that this is a dangerous function, which will force send a response before the end of a route # only use if you know what you're doing! function Send-PodeResponse { diff --git a/src/Public/SSE.ps1 b/src/Public/SSE.ps1 new file mode 100644 index 000000000..e4f7b34a3 --- /dev/null +++ b/src/Public/SSE.ps1 @@ -0,0 +1,471 @@ +<# +.SYNOPSIS +Sets the current HTTP request to a Route to be an SSE connection. + +.DESCRIPTION +Sets the current HTTP request to a Route to be an SSE connection, by sending the required headers back to the client. +The connection can only be configured if the request's Accept header is "text/event-stream", unless Forced. + +.PARAMETER Name +The Name of the SSE connection, which ClientIds will be stored under. + +.PARAMETER Scope +The Scope of the SSE connection, either Local or Global (Default: Global). +- If the Scope is Local, then the SSE connection will only be opened for the duration of the request to a Route that configured it. +- If the Scope is Global, then the SSE connection will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc. + +.PARAMETER RetryDuration +An optional RetryDuration, in milliseconds, for the period of time a browser should wait before reattempting a connection if lost (Default: 0). + +.PARAMETER ClientId +An optional ClientId to use for the SSE connection, this value will be signed if signing is enabled (Default: GUID). + +.PARAMETER AllowAllOrigins +If supplied, then Access-Control-Allow-Origin will be set to * on the response. + +.PARAMETER Force +If supplied, the Accept header of the request will be ignored; attempting to configure an SSE connection even if the header isn't "text/event-stream". + +.EXAMPLE +Set-PodeSseConnection -Name 'Actions' + +.EXAMPLE +Set-PodeSseConnection -Name 'Actions' -Scope Local + +.EXAMPLE +Set-PodeSseConnection -Name 'Actions' -AllowAllOrigins + +.EXAMPLE +Set-PodeSseConnection -Name 'Actions' -ClientId 'my-client-id' +#> +function Set-PodeSseConnection { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [ValidateSet('Local', 'Global')] + [string] + $Scope = 'Global', + + [Parameter()] + [int] + $RetryDuration = 0, + + [Parameter()] + [string] + $ClientId, + + [switch] + $AllowAllOrigins, + + [switch] + $Force + ) + + # check Accept header - unless forcing + if (!$Force -and ((Get-PodeHeader -Name 'Accept') -ine 'text/event-stream')) { + throw 'SSE can only be configured on requests with an Accept header value of text/event-stream' + } + + # generate clientId + $ClientId = New-PodeSseClientId -ClientId $ClientId + + # set adn send SSE headers + $ClientId = $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $RetryDuration, $AllowAllOrigins.IsPresent) + + # create SSE property on WebEvent + $WebEvent.Sse = @{ + Name = $Name + ClientId = $ClientId + LastEventId = Get-PodeHeader -Name 'Last-Event-ID' + IsLocal = ($Scope -ieq 'local') + } +} + +<# +.SYNOPSIS +Send an Event to one or more SSE connections. + +.DESCRIPTION +Send an Event to one or more SSE connections. This can either be: +- Every client for an SSE connection Name +- Specific ClientIds for an SSE connection Name +- The current SSE connection being referenced within $WebEvent.Sse + +.PARAMETER Name +An SSE connection Name. + +.PARAMETER ClientId +An optional array of 1 or more SSE connection ClientIds to send Events to, for the specified SSE connection Name. + +.PARAMETER Id +An optional ID for the Event being sent. + +.PARAMETER EventType +An optional EventType for the Event being sent. + +.PARAMETER Data +The Data for the Event being sent, either as a String or a Hashtable/PSObject. If the latter, it will be converted into JSON. + +.PARAMETER Depth +The Depth to generate the JSON document - the larger this value the worse performance gets. + +.PARAMETER FromEvent +If supplied, the SSE connection Name and ClientId will atttempt to be retrived from $WebEvent.Sse. +These details will be set if Set-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-CLIENT-NAME are set on an HTTP request. + +.EXAMPLE +Send-PodeSseEvent -FromEvent -Data 'This is an event' + +.EXAMPLE +Send-PodeSseEvent -FromEvent -Data @{ Message = 'A message' } + +.EXAMPLE +Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } + +.EXAMPLE +Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } -ID 123 -EventType 'action' +#> +function Send-PodeSseEvent { + [CmdletBinding(DefaultParameterSetName = 'Name')] + param( + [Parameter(Mandatory = $true, ParameterSetName = 'Name')] + [string] + $Name, + + [Parameter(ParameterSetName = 'Name')] + [string[]] + $ClientId, + + [Parameter()] + [string] + $Id, + + [Parameter()] + [string] + $EventType, + + [Parameter(Mandatory = $true)] + $Data, + + [Parameter()] + [int] + $Depth = 10, + + [Parameter(ParameterSetName = 'WebEvent')] + [switch] + $FromEvent + ) + + # do nothing if no value + if (($null -eq $Data) -or ([string]::IsNullOrEmpty($Data))) { + return + } + + # jsonify the value + if ($Data -isnot [string]) { + if ($Depth -le 0) { + $Data = (ConvertTo-Json -InputObject $Data -Compress) + } + else { + $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + } + } + + # send directly back to current connection + if ($FromEvent -and $WebEvent.Sse.IsLocal) { + $WebEvent.Response.SendSseEvent($EventType, $Data, $Id) + return + } + + # from event and global? + if ($FromEvent) { + $Name = $WebEvent.Sse.Name + $ClientId = $WebEvent.Sse.ClientId + } + + # error if no name + if ([string]::IsNullOrEmpty($Name)) { + throw 'An SSE connection Name is required, either from -Name or $WebEvent.Sse.Name' + } + + # send event + $PodeContext.Server.Http.Listener.SendSseEvent($Name, $ClientId, $EventType, $Data, $Id) + + + + + + # mode - are we sending directly back to a client? + # $direct = $false + + # check WebEvent + # if ([string]::IsNullOrEmpty($Name) -and ($null -ne $WebEvent.Sse)) { + # $Name = $WebEvent.Sse.Name + + # if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) { + # $ClientId = $WebEvent.Sse.ClientId + # $direct = $true + # } + # } + + # error if no name + # if ([string]::IsNullOrEmpty($Name)) { + # throw 'An SSE connection name is required, either from -Name or $WebEvent.Sse.Name' + # } + + # jsonify the value + # if ($Data -isnot [string]) { + # if ($Depth -le 0) { + # $Data = (ConvertTo-Json -InputObject $Data -Compress) + # } + # else { + # $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) + # } + # } + + # send message + # if ($direct) { + # $WebEvent.Response.SendSseEvent($EventType, $Data, $Id) + # } + # else { + # $PodeContext.Server.Http.Listener.SendSseEvent($Name, $ClientId, $EventType, $Data, $Id) + # } +} + +<# +.SYNOPSIS +Close one or more SSE connections. + +.DESCRIPTION +Close one or more SSE connections. Either all connections for an SSE connection Name, or specific ClientIds for a Name. + +.PARAMETER Name +The Name of the SSE connection which has the ClientIds for the connections to close. + +.PARAMETER ClientId +An optional array of 1 or more SSE connection ClientIds, that are for the SSE connection Name. +If not supplied, every SSE connection for the supplied Name will be closed. + +.EXAMPLE +Close-PodeSseConnection -Name 'Actions' + +.EXAMPLE +Close-PodeSseConnection -Name 'Actions' -ClientId @('my-client-id', 'my-other'id') +#> +function Close-PodeSseConnection { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string[]] + $ClientId + ) + + $PodeContext.Server.Http.Listener.CloseSseConnection($Name, $ClientId) +} + +<# +.SYNOPSIS +Test if an SSE connection ClientId is validly signed. + +.DESCRIPTION +Test if an SSE connection ClientId is validly signed. + +.PARAMETER ClientId +An optional SSE connection ClientId, if not supplied it will be retrieved from $WebEvent. + +.EXAMPLE +if (Test-PodeSseClientIdValid) { ... } + +.EXAMPLE +if (Test-PodeSseClientIdValid -ClientId 's:my-already-signed-client-id.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ=') { ... } +#> +function Test-PodeSseClientIdSigned { + [CmdletBinding()] + param( + [Parameter()] + [string] + $ClientId + ) + + # get clientId from WebEvent if not passed + if ([string]::IsNullOrEmpty($ClientId)) { + $ClientId = Get-PodeSseClientId + } + + # test if clientId is validly signed + return Test-PodeValueSigned -Value $ClientId -Secret $PodeContext.Server.Sse.Secret -Strict:($PodeContext.Server.Sse.Strict) +} + +<# +.SYNOPSIS +Test if an SSE connection ClientId is valid. + +.DESCRIPTION +Test if an SSE connection ClientId, passed or from $WebEvent, is valid. A ClientId is valid if it's not signed and we're not signing ClientIds, +or if we are signing ClientIds and the ClientId is validly signed. + +.PARAMETER ClientId +An optional SSE connection ClientId, if not supplied it will be retrieved from $WebEvent. + +.EXAMPLE +if (Test-PodeSseClientIdValid) { ... } + +.EXAMPLE +if (Test-PodeSseClientIdValid -ClientId 'my-client-id') { ... } +#> +function Test-PodeSseClientIdValid { + [CmdletBinding()] + param( + [Parameter()] + [string] + $ClientId + ) + + # get clientId from WebEvent if not passed + if ([string]::IsNullOrEmpty($ClientId)) { + $ClientId = Get-PodeSseClientId + } + + # if no clientId, then it's not valid + if ([string]::IsNullOrEmpty($ClientId)) { + return $false + } + + # if we're not signing, then valid if not signed, but invalid if signed + if (!$PodeContext.Server.Sse.Signed) { + return !$ClientId.StartsWith('s:') + } + + # test if clientId is validly signed + return Test-PodeSseClientIdSigned -ClientId $ClientId +} + +<# +.SYNOPSIS +Retrieves an SSE connection ClientId from the current $WebEvent. + +.DESCRIPTION +Retrieves an SSE connection ClientId from the current $WebEvent, which is set via the X-PODE-SSE-CLIENT-ID request header. +This ClientId could be used to send events back to an originating SSE connection. + +.EXAMPLE +$clientId = Get-PodeSseClientId +#> +function Get-PodeSseClientId { + [CmdletBinding()] + param() + + # get clientId from WebEvent + return $WebEvent.Request.SseClientId +} + +<# +.SYNOPSIS +Generate a new SSE connection ClientId. + +.DESCRIPTION +Generate a new SSE connection ClientId, which will be signed if signing enabled. + +.PARAMETER ClientId +An optional SSE connection ClientId to use, if a custom ClientId is needed and required to be signed. + +.EXAMPLE +$clientId = New-PodeSseClientId + +.EXAMPLE +$clientId = New-PodeSseClientId -ClientId 'my-client-id' + +.EXAMPLE +$clientId = New-PodeSseClientId -ClientId 's:my-already-signed-client-id.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ=' +#> +function New-PodeSseClientId { + [CmdletBinding()] + param( + [Parameter()] + [string] + $ClientId + ) + + # if no clientId passed, generate a random guid + if ([string]::IsNullOrEmpty($ClientId)) { + $ClientId = New-PodeGuid -Secure + } + + # if we're signing the clientId, and it's not already signed, then sign it + if ($PodeContext.Server.Sse.Signed -and !$ClientId.StartsWith('s:')) { + $ClientId = Invoke-PodeValueSign -Value $ClientId -Secret $PodeContext.Server.Sse.Secret -Strict:($PodeContext.Server.Sse.Strict) + } + + # return the clientId + return $ClientId +} + +<# +.SYNOPSIS +Enable the signing of SSE connection ClientIds. + +.DESCRIPTION +Enable the signing of SSE connection ClientIds. + +.PARAMETER Secret +An optional Secret to sign ClientIds (Default: random GUID). + +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + +.EXAMPLE +Enable-PodeSseSigning + +.EXAMPLE +Enable-PodeSseSigning -Strict + +.EXAMPLE +Enable-PodeSseSigning -Secret 'Sup3rS3cr37!' -Strict + +.EXAMPLE +Enable-PodeSseSigning -Secret 'Sup3rS3cr37!' +#> +function Enable-PodeSseSigning { + [CmdletBinding()] + param( + [Parameter()] + [string] + $Secret, + + [switch] + $Strict + ) + + # flag that we're signing SSE connections + $PodeContext.Server.Sse.Signed = $true + $PodeContext.Server.Sse.Secret = $Secret + $PodeContext.Server.Sse.Strict = $Strict.IsPresent +} + +<# +.SYNOPSIS +Disable the signing of SSE connection ClientIds. + +.DESCRIPTION +Disable the signing of SSE connection ClientIds. + +.EXAMPLE +Disable-PodeSseSigning +#> +function Disable-PodeSseSigning { + [CmdletBinding()] + param() + + # flag that we're not signing SSE connections + $PodeContext.Server.Sse.Signed = $false + $PodeContext.Server.Sse.Secret = $null + $PodeContext.Server.Sse.Strict = $false +} \ No newline at end of file diff --git a/src/Public/Sessions.ps1 b/src/Public/Sessions.ps1 index 0e1107ff9..bce0dcb6e 100644 --- a/src/Public/Sessions.ps1 +++ b/src/Public/Sessions.ps1 @@ -43,7 +43,7 @@ If supplied, the Secret will be extended using the client request's UserAgent an If supplied, Sessions will be sent back in a header on the Response with the Name supplied. .EXAMPLE -Enable-PodeSessionMiddleware -Duration 120 +Enable-PodeSessionMiddleware -Duration 120 .EXAMPLE Enable-PodeSessionMiddleware -Duration 120 -Extend -Generator { return [System.IO.Path]::GetRandomFileName() } @@ -127,7 +127,7 @@ function Enable-PodeSessionMiddleware { throw 'A Secret is required when using custom session storage' } - $Secret = New-PodeGuid -Secure + $Secret = $PodeContext.Server.BaseSecret } # if no custom storage, use the inmem one @@ -271,13 +271,8 @@ function Get-PodeSessionId { $strict = $PodeContext.Server.Sessions.Info.Strict $secret = $PodeContext.Server.Sessions.Secret - # covert secret to strict mode - if ($strict) { - $secret = ConvertTo-PodeSessionStrictSecret -Secret $secret - } - # sign the value if we have a secret - $sessionId = (Invoke-PodeValueSign -Value $sessionId -Secret $secret) + $sessionId = (Invoke-PodeValueSign -Value $sessionId -Secret $secret -Strict:$strict) } # return the ID From c2f6977ecc6479932afe95095b9b56aa9e6d0a24 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Tue, 19 Mar 2024 22:24:18 +0000 Subject: [PATCH 25/84] #1245: add SSE group and broadcast level control support --- examples/sse.ps1 | 4 +- src/Listener/PodeHttpRequest.cs | 2 + src/Listener/PodeListener.cs | 14 +- src/Listener/PodeResponse.cs | 11 +- src/Listener/PodeServerEvent.cs | 20 ++- src/Pode.psd1 | 6 +- src/Private/Context.ps1 | 7 +- src/Private/FileWatchers.ps1 | 1 + src/Private/PodeServer.ps1 | 3 + src/Private/Schedules.ps1 | 1 + src/Private/Serverless.ps1 | 2 + src/Private/ServiceServer.ps1 | 1 + src/Private/SmtpServer.ps1 | 1 + src/Private/Tasks.ps1 | 1 + src/Private/TcpServer.ps1 | 1 + src/Private/Timers.ps1 | 1 + src/Private/WebSockets.ps1 | 1 + src/Public/Cookies.ps1 | 12 ++ src/Public/Headers.ps1 | 18 ++ src/Public/SSE.ps1 | 280 +++++++++++++++++++++++--------- 20 files changed, 296 insertions(+), 91 deletions(-) diff --git a/examples/sse.ps1 b/examples/sse.ps1 index 8e6d26c88..0fa57b0bd 100644 --- a/examples/sse.ps1 +++ b/examples/sse.ps1 @@ -14,7 +14,7 @@ Start-PodeServer -Threads 3 { # open local sse connection, and send back data Add-PodeRoute -Method Get -Path '/data' -ScriptBlock { - Set-PodeSseConnection -Name 'Data' -Scope Local + ConvertTo-PodeSseConnection -Name 'Data' -Scope Local Send-PodeSseEvent -Id 1234 -EventType Action -Data 'hello, there!' Start-Sleep -Seconds 3 Send-PodeSseEvent -Id 1337 -EventType BoldOne -Data 'general kenobi' @@ -26,7 +26,7 @@ Start-PodeServer -Threads 3 { } Add-PodeRoute -Method Get -Path '/sse' -ScriptBlock { - Set-PodeSseConnection -Name 'Test' + ConvertTo-PodeSseConnection -Name 'Test' } Add-PodeTimer -Name 'SendEvent' -Interval 10 -ScriptBlock { diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs index bf9c43958..42a156717 100644 --- a/src/Listener/PodeHttpRequest.cs +++ b/src/Listener/PodeHttpRequest.cs @@ -36,6 +36,7 @@ public class PodeHttpRequest : PodeRequest public string SseClientId { get; private set; } public string SseClientName { get; private set; } + public string SseClientGroup { get; private set; } public bool HasSseClientId { get => !string.IsNullOrEmpty(SseClientId); @@ -304,6 +305,7 @@ private int ParseHeaders(string[] reqLines, string newline) if (HasSseClientId) { SseClientName = $"{Headers["X-Pode-Sse-Client-Name"]}"; + SseClientGroup = $"{Headers["X-Pode-Sse-Client-Group"]}"; } // keep-alive? diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index 382028370..5761b7826 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -135,7 +135,7 @@ public void AddSseConnection(PodeServerEvent sse) } } - public void SendSseEvent(string name, string[] clientIds, string eventType, string data, string id = null) + public void SendSseEvent(string name, string[] groups, string[] clientIds, string eventType, string data, string id = null) { Task.Factory.StartNew(() => { if (!ServerEvents.ContainsKey(name)) @@ -155,12 +155,15 @@ public void SendSseEvent(string name, string[] clientIds, string eventType, stri continue; } - ServerEvents[name][clientId].Context.Response.SendSseEvent(eventType, data, id); + if (ServerEvents[name][clientId].IsForGroup(groups)) + { + ServerEvents[name][clientId].Context.Response.SendSseEvent(eventType, data, id); + } } }, CancellationToken); } - public void CloseSseConnection(string name, string[] clientIds) + public void CloseSseConnection(string name, string[] groups, string[] clientIds) { Task.Factory.StartNew(() => { if (!ServerEvents.ContainsKey(name)) @@ -180,7 +183,10 @@ public void CloseSseConnection(string name, string[] clientIds) continue; } - ServerEvents[name][clientId].Context.Response.CloseSseConnection(); + if (ServerEvents[name][clientId].IsForGroup(groups)) + { + ServerEvents[name][clientId].Context.Response.CloseSseConnection(); + } } }, CancellationToken); } diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index 1842943aa..a54e082b4 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -193,7 +193,7 @@ public void Flush() } } - public string SetSseConnection(PodeSseScope scope, string clientId, string name, int retry, bool allowAllOrigins) + public string SetSseConnection(PodeSseScope scope, string clientId, string name, string group, int retry, bool allowAllOrigins) { // do nothing for no scope if (scope == PodeSseScope.None) @@ -225,15 +225,20 @@ public string SetSseConnection(PodeSseScope scope, string clientId, string name, Headers.Set("X-Pode-Sse-Client-Id", clientId); Headers.Set("X-Pode-Sse-Client-Name", name); + if (!string.IsNullOrEmpty(group)) + { + Headers.Set("X-Pode-Sse-Client-Group", group); + } + // send headers, and open event Send(); SendSseRetry(retry); - SendSseEvent("pode.open", $"{{\"clientId\":\"{clientId}\",\"name\":\"{name}\"}}"); + SendSseEvent("pode.open", $"{{\"clientId\":\"{clientId}\",\"group\":\"{group}\",\"name\":\"{name}\"}}"); // if global, cache connection in listener if (scope == PodeSseScope.Global) { - Context.Listener.AddSseConnection(new PodeServerEvent(Context, name, clientId)); + Context.Listener.AddSseConnection(new PodeServerEvent(Context, name, group, clientId)); } // return clientId diff --git a/src/Listener/PodeServerEvent.cs b/src/Listener/PodeServerEvent.cs index 34b8e42ec..a251b27c5 100644 --- a/src/Listener/PodeServerEvent.cs +++ b/src/Listener/PodeServerEvent.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; namespace Pode { @@ -6,17 +7,34 @@ public class PodeServerEvent : IDisposable { public PodeContext Context { get; private set; } public string Name { get; private set; } + public string Group { get; private set; } public string ClientId { get; private set; } public DateTime Timestamp { get; private set; } - public PodeServerEvent(PodeContext context, string name, string clientId) + public PodeServerEvent(PodeContext context, string name, string group, string clientId) { Context = context; Name = name; + Group = group; ClientId = clientId; Timestamp = DateTime.UtcNow; } + public bool IsForGroup(string[] groups) + { + if (groups == default(string[]) || groups.Length == 0) + { + return true; + } + + if (string.IsNullOrEmpty(Group)) + { + return false; + } + + return groups.Any(x => x.Equals(Group, StringComparison.OrdinalIgnoreCase)); + } + public void Dispose() { Context.Dispose(true); diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 253884262..a25de0ae6 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -91,15 +91,17 @@ 'Send-PodeResponse', # sse - 'Set-PodeSseConnection', + 'ConvertTo-PodeSseConnection', 'Send-PodeSseEvent', 'Close-PodeSseConnection', 'Test-PodeSseClientIdSigned', 'Test-PodeSseClientIdValid', - 'Get-PodeSseClientId', 'New-PodeSseClientId', 'Enable-PodeSseSigning', 'Disable-PodeSseSigning', + 'Set-PodeSseBroadcastLevel', + 'Get-PodeSseBroadcastLevel', + 'Test-PodeSseBroadcastLevel', # utility helpers 'Close-PodeDisposable', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 73c31c40d..bccdfc996 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -159,9 +159,10 @@ function New-PodeContext { } $ctx.Server.Sse = @{ - Signed = $false - Secret = $null - Strict = $false + Signed = $false + Secret = $null + Strict = $false + BroadcastLevel = @{} } $ctx.Server.WebSockets = @{ diff --git a/src/Private/FileWatchers.ps1 b/src/Private/FileWatchers.ps1 index 85bc88c81..b768ef5a7 100644 --- a/src/Private/FileWatchers.ps1 +++ b/src/Private/FileWatchers.ps1 @@ -91,6 +91,7 @@ function Start-PodeFileWatcherRunspace { Parameters = @{} Lockable = $PodeContext.Threading.Lockables.Global Timestamp = [datetime]::UtcNow + Metadata = @{} } # do we have any parameters? diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index e2deb0dc1..dbebe163f 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -147,6 +147,7 @@ function Start-PodeWebServer { AcceptEncoding = $null Ranges = $null Sse = $null + Metadata = @{} } # if iis, and we have an app path, alter it @@ -181,6 +182,7 @@ function Start-PodeWebServer { $WebEvent.Sse = @{ Name = $WebEvent.Request.SseClientName + Group = $WebEvent.Request.SseClientGroup ClientId = $WebEvent.Request.SseClientId LastEventId = $null IsLocal = $false @@ -379,6 +381,7 @@ function Start-PodeWebServer { ClientId = $context.Signal.ClientId Timestamp = $context.Timestamp Streamed = $true + Metadata = @{} } # endpoint name diff --git a/src/Private/Schedules.ps1 b/src/Private/Schedules.ps1 index bcdea1092..a59981ef8 100644 --- a/src/Private/Schedules.ps1 +++ b/src/Private/Schedules.ps1 @@ -169,6 +169,7 @@ function Invoke-PodeInternalScheduleLogic { Event = @{ Lockable = $PodeContext.Threading.Lockables.Global Sender = $Schedule + Metadata = @{} } } diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index 530db2447..0ff62e4fa 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -54,6 +54,7 @@ function Start-PodeAzFuncServer { TransferEncoding = $null AcceptEncoding = $null Ranges = $null + Metadata = @{} } $WebEvent.Endpoint.Address = ((Get-PodeHeader -Name 'host') -split ':')[0] @@ -180,6 +181,7 @@ function Start-PodeAwsLambdaServer { TransferEncoding = $null AcceptEncoding = $null Ranges = $null + Metadata = @{} } $WebEvent.Endpoint.Protocol = (Get-PodeHeader -Name 'X-Forwarded-Proto') diff --git a/src/Private/ServiceServer.ps1 b/src/Private/ServiceServer.ps1 index 7e0d66b2d..9f39bb535 100644 --- a/src/Private/ServiceServer.ps1 +++ b/src/Private/ServiceServer.ps1 @@ -14,6 +14,7 @@ function Start-PodeServiceServer { # the event object $ServiceEvent = @{ Lockable = $PodeContext.Threading.Lockables.Global + Metadata = @{} } # invoke the service handlers diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index e08d7cbfa..9ffe337a0 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -112,6 +112,7 @@ function Start-PodeSmtpServer { Name = $null } Timestamp = [datetime]::UtcNow + Metadata = @{} } # endpoint name diff --git a/src/Private/Tasks.ps1 b/src/Private/Tasks.ps1 index 2ac4245a0..1ab6ad6bb 100644 --- a/src/Private/Tasks.ps1 +++ b/src/Private/Tasks.ps1 @@ -80,6 +80,7 @@ function Invoke-PodeInternalTask { Event = @{ Lockable = $PodeContext.Threading.Lockables.Global Sender = $Task + Metadata = @{} } } diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 0581b1f78..5eb99763b 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -98,6 +98,7 @@ function Start-PodeTcpServer { } Parameters = $null Timestamp = [datetime]::UtcNow + Metadata = @{} } # endpoint name diff --git a/src/Private/Timers.ps1 b/src/Private/Timers.ps1 index a6f1136bf..ecca314ea 100644 --- a/src/Private/Timers.ps1 +++ b/src/Private/Timers.ps1 @@ -75,6 +75,7 @@ function Invoke-PodeInternalTimer { $global:TimerEvent = @{ Lockable = $PodeContext.Threading.Lockables.Global Sender = $Timer + Metadata = @{} } # add main timer args diff --git a/src/Private/WebSockets.ps1 b/src/Private/WebSockets.ps1 index 16fbf5cff..f72aef50a 100644 --- a/src/Private/WebSockets.ps1 +++ b/src/Private/WebSockets.ps1 @@ -64,6 +64,7 @@ function Start-PodeWebSocketRunspace { Files = $null Lockable = $PodeContext.Threading.Lockables.Global Timestamp = [datetime]::UtcNow + Metadata = @{} } # find the websocket definition diff --git a/src/Public/Cookies.ps1 b/src/Public/Cookies.ps1 index d8a951dc6..a9dd16e6d 100644 --- a/src/Public/Cookies.ps1 +++ b/src/Public/Cookies.ps1 @@ -29,6 +29,9 @@ Inform browsers to remove the cookie. .PARAMETER Secure Only allow the cookie on secure (HTTPS) connections. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Set-PodeCookie -Name 'Views' -Value 2 @@ -117,6 +120,9 @@ The name of the cookie to retrieve. .PARAMETER Secret The secret used to unsign the cookie's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .PARAMETER Raw If supplied, the cookie returned will be the raw .NET Cookie object for manipulation. @@ -179,6 +185,9 @@ The name of the cookie to retrieve. .PARAMETER Secret The secret used to unsign the cookie's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Get-PodeCookieValue -Name 'Views' @@ -284,6 +293,9 @@ The name of the cookie to test. .PARAMETER Secret A secret to use for attempting to unsign the cookie's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Test-PodeCookieSigned -Name 'Views' -Secret 'hunter2' #> diff --git a/src/Public/Headers.ps1 b/src/Public/Headers.ps1 index b52a638c8..84a907b2f 100644 --- a/src/Public/Headers.ps1 +++ b/src/Public/Headers.ps1 @@ -14,6 +14,9 @@ The value to set against the header. .PARAMETER Secret If supplied, the secret with which to sign the header's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Add-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33' #> @@ -63,6 +66,9 @@ A hashtable of headers to be appended. .PARAMETER Secret If supplied, the secret with which to sign the header values. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Add-PodeHeaderBulk -Values @{ Name1 = 'Value1'; Name2 = 'Value2' } #> @@ -138,6 +144,9 @@ The name of the header to retrieve. .PARAMETER Secret The secret used to unsign the header's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Get-PodeHeader -Name 'X-AuthToken' #> @@ -184,6 +193,9 @@ The value to set against the header. .PARAMETER Secret If supplied, the secret with which to sign the header's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Set-PodeHeader -Name 'X-AuthToken' -Value 'AA-BB-CC-33' #> @@ -233,6 +245,9 @@ A hashtable of headers to be set. .PARAMETER Secret If supplied, the secret with which to sign the header values. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Set-PodeHeaderBulk -Values @{ Name1 = 'Value1'; Name2 = 'Value2' } #> @@ -282,6 +297,9 @@ The name of the header to test. .PARAMETER Secret A secret to use for attempting to unsign the header's value. +.PARAMETER Strict +If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. + .EXAMPLE Test-PodeHeaderSigned -Name 'X-Header-Name' -Secret 'hunter2' #> diff --git a/src/Public/SSE.ps1 b/src/Public/SSE.ps1 index e4f7b34a3..e0dcfb1c0 100644 --- a/src/Public/SSE.ps1 +++ b/src/Public/SSE.ps1 @@ -1,14 +1,17 @@ <# .SYNOPSIS -Sets the current HTTP request to a Route to be an SSE connection. +Converts the current HTTP request to a Route to be an SSE connection. .DESCRIPTION -Sets the current HTTP request to a Route to be an SSE connection, by sending the required headers back to the client. +Converts the current HTTP request to a Route to be an SSE connection, by sending the required headers back to the client. The connection can only be configured if the request's Accept header is "text/event-stream", unless Forced. .PARAMETER Name The Name of the SSE connection, which ClientIds will be stored under. +.PARAMETER Group +An optional Group for this SSE connection, to enable broadcasting events to all connections for an SSE connection name in a Group. + .PARAMETER Scope The Scope of the SSE connection, either Local or Global (Default: Global). - If the Scope is Local, then the SSE connection will only be opened for the duration of the request to a Route that configured it. @@ -27,24 +30,31 @@ If supplied, then Access-Control-Allow-Origin will be set to * on the response. If supplied, the Accept header of the request will be ignored; attempting to configure an SSE connection even if the header isn't "text/event-stream". .EXAMPLE -Set-PodeSseConnection -Name 'Actions' +ConvertTo-PodeSseConnection -Name 'Actions' + +.EXAMPLE +ConvertTo-PodeSseConnection -Name 'Actions' -Scope Local .EXAMPLE -Set-PodeSseConnection -Name 'Actions' -Scope Local +ConvertTo-PodeSseConnection -Name 'Actions' -Group 'admins' .EXAMPLE -Set-PodeSseConnection -Name 'Actions' -AllowAllOrigins +ConvertTo-PodeSseConnection -Name 'Actions' -AllowAllOrigins .EXAMPLE -Set-PodeSseConnection -Name 'Actions' -ClientId 'my-client-id' +ConvertTo-PodeSseConnection -Name 'Actions' -ClientId 'my-client-id' #> -function Set-PodeSseConnection { +function ConvertTo-PodeSseConnection { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $Name, + [Parameter()] + [string] + $Group, + [Parameter()] [ValidateSet('Local', 'Global')] [string] @@ -73,12 +83,13 @@ function Set-PodeSseConnection { # generate clientId $ClientId = New-PodeSseClientId -ClientId $ClientId - # set adn send SSE headers - $ClientId = $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $RetryDuration, $AllowAllOrigins.IsPresent) + # set and send SSE headers + $ClientId = $WebEvent.Response.SetSseConnection($Scope, $ClientId, $Name, $Group, $RetryDuration, $AllowAllOrigins.IsPresent) # create SSE property on WebEvent $WebEvent.Sse = @{ Name = $Name + Group = $Group ClientId = $ClientId LastEventId = Get-PodeHeader -Name 'Last-Event-ID' IsLocal = ($Scope -ieq 'local') @@ -98,6 +109,9 @@ Send an Event to one or more SSE connections. This can either be: .PARAMETER Name An SSE connection Name. +.PARAMETER Group +An optional array of 1 or more SSE connection Groups to send Events to, for the specified SSE connection Name. + .PARAMETER ClientId An optional array of 1 or more SSE connection ClientIds to send Events to, for the specified SSE connection Name. @@ -115,7 +129,7 @@ The Depth to generate the JSON document - the larger this value the worse perfor .PARAMETER FromEvent If supplied, the SSE connection Name and ClientId will atttempt to be retrived from $WebEvent.Sse. -These details will be set if Set-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-CLIENT-NAME are set on an HTTP request. +These details will be set if ConvertTo-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-CLIENT-NAME are set on an HTTP request. .EXAMPLE Send-PodeSseEvent -FromEvent -Data 'This is an event' @@ -126,6 +140,9 @@ Send-PodeSseEvent -FromEvent -Data @{ Message = 'A message' } .EXAMPLE Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } +.EXAMPLE +Send-PodeSseEvent -Name 'Actions' -Group 'admins' -Data @{ Message = 'A message' } + .EXAMPLE Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } -ID 123 -EventType 'action' #> @@ -138,7 +155,11 @@ function Send-PodeSseEvent { [Parameter(ParameterSetName = 'Name')] [string[]] - $ClientId, + $Group = $null, + + [Parameter(ParameterSetName = 'Name')] + [string[]] + $ClientId = $null, [Parameter()] [string] @@ -184,6 +205,7 @@ function Send-PodeSseEvent { # from event and global? if ($FromEvent) { $Name = $WebEvent.Sse.Name + $Group = $WebEvent.Sse.Group $ClientId = $WebEvent.Sse.ClientId } @@ -192,48 +214,13 @@ function Send-PodeSseEvent { throw 'An SSE connection Name is required, either from -Name or $WebEvent.Sse.Name' } - # send event - $PodeContext.Server.Http.Listener.SendSseEvent($Name, $ClientId, $EventType, $Data, $Id) - - - - - - # mode - are we sending directly back to a client? - # $direct = $false - - # check WebEvent - # if ([string]::IsNullOrEmpty($Name) -and ($null -ne $WebEvent.Sse)) { - # $Name = $WebEvent.Sse.Name - - # if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) { - # $ClientId = $WebEvent.Sse.ClientId - # $direct = $true - # } - # } - - # error if no name - # if ([string]::IsNullOrEmpty($Name)) { - # throw 'An SSE connection name is required, either from -Name or $WebEvent.Sse.Name' - # } + # check if broadcast level + if (!(Test-PodeSseBroadcastLevel -Name $Name -Group $Group -ClientId $ClientId)) { + throw "SSE failed to broadcast due to defined SSE broadcast level for $($Name): $(Get-PodeSseBroadcastLevel -Name $Name)" + } - # jsonify the value - # if ($Data -isnot [string]) { - # if ($Depth -le 0) { - # $Data = (ConvertTo-Json -InputObject $Data -Compress) - # } - # else { - # $Data = (ConvertTo-Json -InputObject $Data -Depth $Depth -Compress) - # } - # } - - # send message - # if ($direct) { - # $WebEvent.Response.SendSseEvent($EventType, $Data, $Id) - # } - # else { - # $PodeContext.Server.Http.Listener.SendSseEvent($Name, $ClientId, $EventType, $Data, $Id) - # } + # send event + $PodeContext.Server.Http.Listener.SendSseEvent($Name, $Group, $ClientId, $EventType, $Data, $Id) } <# @@ -244,7 +231,10 @@ Close one or more SSE connections. Close one or more SSE connections. Either all connections for an SSE connection Name, or specific ClientIds for a Name. .PARAMETER Name -The Name of the SSE connection which has the ClientIds for the connections to close. +The Name of the SSE connection which has the ClientIds for the connections to close. If supplied on its own, all connections will be closed. + +.PARAMETER Group +An optional array of 1 or more SSE connection Groups, that are for the SSE connection Name. If supplied without any ClientIds, then all connections for the Group(s) will be closed. .PARAMETER ClientId An optional array of 1 or more SSE connection ClientIds, that are for the SSE connection Name. @@ -253,6 +243,9 @@ If not supplied, every SSE connection for the supplied Name will be closed. .EXAMPLE Close-PodeSseConnection -Name 'Actions' +.EXAMPLE +Close-PodeSseConnection -Name 'Actions' -Group 'admins' + .EXAMPLE Close-PodeSseConnection -Name 'Actions' -ClientId @('my-client-id', 'my-other'id') #> @@ -265,10 +258,14 @@ function Close-PodeSseConnection { [Parameter()] [string[]] - $ClientId + $Group = $null, + + [Parameter()] + [string[]] + $ClientId = $null ) - $PodeContext.Server.Http.Listener.CloseSseConnection($Name, $ClientId) + $PodeContext.Server.Http.Listener.CloseSseConnection($Name, $Group, $ClientId) } <# @@ -297,7 +294,7 @@ function Test-PodeSseClientIdSigned { # get clientId from WebEvent if not passed if ([string]::IsNullOrEmpty($ClientId)) { - $ClientId = Get-PodeSseClientId + $ClientId = $WebEvent.Request.SseClientId } # test if clientId is validly signed @@ -331,7 +328,7 @@ function Test-PodeSseClientIdValid { # get clientId from WebEvent if not passed if ([string]::IsNullOrEmpty($ClientId)) { - $ClientId = Get-PodeSseClientId + $ClientId = $WebEvent.Request.SseClientId } # if no clientId, then it's not valid @@ -348,25 +345,6 @@ function Test-PodeSseClientIdValid { return Test-PodeSseClientIdSigned -ClientId $ClientId } -<# -.SYNOPSIS -Retrieves an SSE connection ClientId from the current $WebEvent. - -.DESCRIPTION -Retrieves an SSE connection ClientId from the current $WebEvent, which is set via the X-PODE-SSE-CLIENT-ID request header. -This ClientId could be used to send events back to an originating SSE connection. - -.EXAMPLE -$clientId = Get-PodeSseClientId -#> -function Get-PodeSseClientId { - [CmdletBinding()] - param() - - # get clientId from WebEvent - return $WebEvent.Request.SseClientId -} - <# .SYNOPSIS Generate a new SSE connection ClientId. @@ -468,4 +446,154 @@ function Disable-PodeSseSigning { $PodeContext.Server.Sse.Signed = $false $PodeContext.Server.Sse.Secret = $null $PodeContext.Server.Sse.Strict = $false +} + +<# +.SYNOPSIS +Set an allowed broadcast level for SSE connections. + +.DESCRIPTION +Set an allowed broadcast level for SSE connections, either for all SSE connection names or specific ones. + +.PARAMETER Name +An optional Name for an SSE connection (default: *). + +.PARAMETER Type +The broadcast level Type for the SSE connection. +Name = Allow broadcasting at all levels, including broadcasting to all Groups and/or ClientIds for an SSE connection Name. +Group = Allow broadcasting to only Groups or specific ClientIds. If neither Groups nor ClientIds are supplied, sending an event will fail. +ClientId = Allow broadcasting to only ClientIds. If no ClientIds are supplied, sending an event will fail. + +.EXAMPLE +Set-PodeSseBroadcastLevel -Type Name + +.EXAMPLE +Set-PodeSseBroadcastLevel -Type Group + +.EXAMPLE +Set-PodeSseBroadcastLevel -Name 'Actions' -Type ClientId +#> +function Set-PodeSseBroadcastLevel { + [CmdletBinding()] + param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] + $Name = '*', + + [Parameter()] + [ValidateSet('Name', 'Group', 'ClientId')] + [string] + $Type + ) + + $PodeContext.Server.Sse.BroadcastLevel[$Name] = $Type.ToLowerInvariant() +} + +<# +.SYNOPSIS +Retrieve the broadcast level for an SSE connection Name. + +.DESCRIPTION +Retrieve the broadcast level for an SSE connection Name. If one hasn't been set explicitly then the base level will be checked. +If no broadcasting level have been set at all, then the "Name" level will be returned. + +.PARAMETER Name +The Name of an SSE connection. + +.EXAMPLE +$level = Get-PodeSseBroadcastLevel -Name 'Actions' +#> +function Get-PodeSseBroadcastLevel { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + # if no levels, return null + if ($PodeContext.Server.Sse.BroadcastLevel.Count -eq 0) { + return 'name' + } + + # get level or default level + $level = $PodeContext.Server.Sse.BroadcastLevel[$Name] + if ([string]::IsNullOrEmpty($level)) { + $level = $PodeContext.Server.Sse.BroadcastLevel['*'] + } + + if ([string]::IsNullOrEmpty($level)) { + $level = 'name' + } + + # return level + return $level +} + +<# +.SYNOPSIS +Test if an SSE connection can be broadcasted to, given the Name, Group, and ClientIds. + +.DESCRIPTION +Test if an SSE connection can be broadcasted to, given the Name, Group, and ClientIds. + +.PARAMETER Name +The Name of the SSE connection. + +.PARAMETER Group +An array of 1 or more Groups. + +.PARAMETER ClientId +An array of 1 or more ClientIds. + +.EXAMPLE +if (Test-PodeSseBroadcastLevel -Name 'Actions') { ... } + +.EXAMPLE +if (Test-PodeSseBroadcastLevel -Name 'Actions' -Group 'admins') { ... } + +.EXAMPLE +if (Test-PodeSseBroadcastLevel -Name 'Actions' -ClientId 'my-client-id') { ... } +#> +function Test-PodeSseBroadcastLevel { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter()] + [string[]] + $Group, + + [Parameter()] + [string[]] + $ClientId + ) + + # get level, and if no level or level=name, return true + $level = Get-PodeSseBroadcastLevel -Name $Name + if ([string]::IsNullOrEmpty($level) -or ($level -ieq 'name')) { + return $true + } + + # if level=group, return false if no groups or clientIds + # if level=clientId, return false if no clientIds + switch ($level) { + 'group' { + if ((($null -eq $Group) -or ($Group.Length -eq 0)) -and (($null -eq $ClientId) -or ($ClientId.Length -eq 0))) { + return $false + } + } + + 'clientid' { + if (($null -eq $ClientId) -or ($ClientId.Length -eq 0)) { + return $false + } + } + } + + # valid, return true + return $true } \ No newline at end of file From dd669797cd263273fdb83c0104742354dde2df41 Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Fri, 22 Mar 2024 20:34:08 +0000 Subject: [PATCH 26/84] #1245: move default secret to function instead of forcing, and fix tests --- src/Pode.psd1 | 1 + src/Private/Context.ps1 | 4 +- src/Private/Cryptography.ps1 | 19 ++-- src/Public/Cookies.ps1 | 2 +- src/Public/Core.ps1 | 18 ++++ src/Public/SSE.ps1 | 4 +- src/Public/Sessions.ps1 | 2 +- tests/unit/Cookies.Tests.ps1 | 187 ++++++++++++++++++++--------------- tests/unit/Server.Tests.ps1 | 3 + 9 files changed, 144 insertions(+), 96 deletions(-) diff --git a/src/Pode.psd1 b/src/Pode.psd1 index a25de0ae6..9af1a5eea 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -286,6 +286,7 @@ 'Add-PodeEndpoint', 'Get-PodeEndpoint', 'Pode', + 'Get-PodeServerDefaultSecret', # openapi 'Enable-PodeOpenApi', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index bccdfc996..8b85e90ef 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -97,8 +97,8 @@ function New-PodeContext { $ctx.Receivers = @() $ctx.Watchers = @() - # base secret that can used when needed, and a secret isn't supplied - $ctx.Server.BaseSecret = New-PodeGuid -Secure + # default secret that can used when needed, and a secret isn't supplied + $ctx.Server.DefaultSecret = New-PodeGuid -Secure # list of timers/schedules/tasks/fim $ctx.Timers = @{ diff --git a/src/Private/Cryptography.ps1 b/src/Private/Cryptography.ps1 index 8aa8faeb8..76805e20a 100644 --- a/src/Private/Cryptography.ps1 +++ b/src/Private/Cryptography.ps1 @@ -196,7 +196,8 @@ function Invoke-PodeValueSign { [string] $Value, - [Parameter()] + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string] $Secret, @@ -204,10 +205,6 @@ function Invoke-PodeValueSign { $Strict ) - if ([string]::IsNullOrEmpty($Secret)) { - $Secret = $PodeContext.Server.BaseSecret - } - if ($Strict) { $Secret = ConvertTo-PodeStrictSecret -Secret $Secret } @@ -222,7 +219,8 @@ function Invoke-PodeValueUnsign { [string] $Value, - [Parameter()] + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string] $Secret, @@ -242,10 +240,6 @@ function Invoke-PodeValueUnsign { return $null } - if ([string]::IsNullOrEmpty($Secret)) { - $Secret = $PodeContext.Server.BaseSecret - } - if ($Strict) { $Secret = ConvertTo-PodeStrictSecret -Secret $Secret } @@ -263,11 +257,12 @@ function Invoke-PodeValueUnsign { function Test-PodeValueSigned { param( - [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Parameter(ValueFromPipeline = $true)] [string] $Value, - [Parameter()] + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] [string] $Secret, diff --git a/src/Public/Cookies.ps1 b/src/Public/Cookies.ps1 index a9dd16e6d..6bff3e2a0 100644 --- a/src/Public/Cookies.ps1 +++ b/src/Public/Cookies.ps1 @@ -316,7 +316,7 @@ function Test-PodeCookieSigned { ) $cookie = $WebEvent.Cookies[$Name] - if ($null -eq $cookie) { + if (($null -eq $cookie) -or [string]::IsNullOrEmpty($cookie.Value)) { return $false } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index 63449a0cc..afe1aefc8 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -394,6 +394,24 @@ function Start-PodeStaticServer { } } +<# +.SYNOPSIS +A default server secret that can be for signing values like Session, Cookies, or SSE IDs. + +.DESCRIPTION +A default server secret that can be for signing values like Session, Cookies, or SSE IDs. This secret is regenerated +on every server start and restart. + +.EXAMPLE +$secret = Get-PodeServerDefaultSecret +#> +function Get-PodeServerDefaultSecret { + [CmdletBinding()] + param() + + return $PodeContext.Server.DefaultSecret +} + <# .SYNOPSIS The CLI for Pode, to initialise, build and start your Server. diff --git a/src/Public/SSE.ps1 b/src/Public/SSE.ps1 index e0dcfb1c0..bd2f6de68 100644 --- a/src/Public/SSE.ps1 +++ b/src/Public/SSE.ps1 @@ -394,7 +394,7 @@ Enable the signing of SSE connection ClientIds. Enable the signing of SSE connection ClientIds. .PARAMETER Secret -An optional Secret to sign ClientIds (Default: random GUID). +A Secret to sign ClientIds, Get-PodeServerDefaultSecret can be used. .PARAMETER Strict If supplied, the Secret will be extended using the client request's UserAgent and RemoteIPAddress. @@ -414,7 +414,7 @@ Enable-PodeSseSigning -Secret 'Sup3rS3cr37!' function Enable-PodeSseSigning { [CmdletBinding()] param( - [Parameter()] + [Parameter(Mandatory = $true)] [string] $Secret, diff --git a/src/Public/Sessions.ps1 b/src/Public/Sessions.ps1 index bce0dcb6e..55b3991e3 100644 --- a/src/Public/Sessions.ps1 +++ b/src/Public/Sessions.ps1 @@ -127,7 +127,7 @@ function Enable-PodeSessionMiddleware { throw 'A Secret is required when using custom session storage' } - $Secret = $PodeContext.Server.BaseSecret + $Secret = Get-PodeServerDefaultSecret } # if no custom storage, use the inmem one diff --git a/tests/unit/Cookies.Tests.ps1 b/tests/unit/Cookies.Tests.ps1 index 5d599c1a0..8a657f006 100644 --- a/tests/unit/Cookies.Tests.ps1 +++ b/tests/unit/Cookies.Tests.ps1 @@ -6,7 +6,8 @@ Describe 'Test-PodeCookie' { It 'Returns true' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } } + } + } Test-PodeCookie -Name 'test' | Should Be $true } @@ -25,8 +26,9 @@ Describe 'Test-PodeCookie' { Describe 'Test-PodeCookieSigned' { It 'Returns false for no value' { $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ } - } } + 'test' = @{ } + } + } Test-PodeCookieSigned -Name 'test' | Should Be $false } @@ -40,8 +42,9 @@ Describe 'Test-PodeCookieSigned' { Mock Invoke-PodeValueUnsign { return $null } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } { Test-PodeCookieSigned -Name 'test' } | Should Throw 'argument is null' Assert-MockCalled Invoke-PodeValueUnsign -Times 0 -Scope It @@ -51,8 +54,9 @@ Describe 'Test-PodeCookieSigned' { Mock Invoke-PodeValueUnsign { return $null } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } Test-PodeCookieSigned -Name 'test' -Secret 'key' | Should Be $false Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It @@ -62,8 +66,9 @@ Describe 'Test-PodeCookieSigned' { Mock Invoke-PodeValueUnsign { return 'value' } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } Test-PodeCookieSigned -Name 'test' -Secret 'key' | Should Be $true Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It @@ -73,14 +78,17 @@ Describe 'Test-PodeCookieSigned' { Mock Invoke-PodeValueUnsign { return 'value' } $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Secrets' = @{ - 'global' = 'key' - } - } } } + 'Cookies' = @{ 'Secrets' = @{ + 'global' = 'key' + } + } + } + } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } Test-PodeCookieSigned -Name 'test' -Secret (Get-PodeCookieSecret -Global) | Should Be $true Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It @@ -90,24 +98,27 @@ Describe 'Test-PodeCookieSigned' { Describe 'Get-PodeCookie' { It 'Returns null for no value' { $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ } - } } + 'test' = @{ } + } + } Get-PodeCookie -Name 'test' | Should Be $null } It 'Returns null for not existing' { $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ } - } } + 'test' = @{ } + } + } Get-PodeCookie -Name 'test' | Should Be $null } It 'Returns a cookie, with no secret' { $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } $c = Get-PodeCookie -Name 'test' $c | Should Not Be $null @@ -118,8 +129,9 @@ Describe 'Get-PodeCookie' { Mock Invoke-PodeValueUnsign { return $null } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } $c = Get-PodeCookie -Name 'test' -Secret 'key' $c | Should Not Be $null @@ -132,8 +144,9 @@ Describe 'Get-PodeCookie' { Mock Invoke-PodeValueUnsign { return 'some-id' } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } $c = Get-PodeCookie -Name 'test' -Secret 'key' $c | Should Not Be $null @@ -146,14 +159,17 @@ Describe 'Get-PodeCookie' { Mock Invoke-PodeValueUnsign { return 'some-id' } $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Secrets' = @{ - 'global' = 'key' - } - } } } + 'Cookies' = @{ 'Secrets' = @{ + 'global' = 'key' + } + } + } + } $WebEvent = @{ 'Cookies' = @{ - 'test' = @{ 'Value' = 'example' } - } } + 'test' = @{ 'Value' = 'example' } + } + } $c = Get-PodeCookie -Name 'test' -Secret (Get-PodeCookieSecret -Global) $c | Should Not Be $null @@ -166,8 +182,9 @@ Describe 'Get-PodeCookie' { Describe 'Set-PodeCookie' { It 'Adds simple cookie to response' { $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; 'PendingCookies' = @{} } + 'Headers' = @{} + }; 'PendingCookies' = @{} + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -196,8 +213,9 @@ Describe 'Set-PodeCookie' { Mock Invoke-PodeValueSign { return 'some-id' } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; 'PendingCookies' = @{} } + 'Headers' = @{} + }; 'PendingCookies' = @{} + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -224,14 +242,17 @@ Describe 'Set-PodeCookie' { Mock Invoke-PodeValueSign { return 'some-id' } $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Secrets' = @{ - 'global' = 'key' - } - } } } + 'Cookies' = @{ 'Secrets' = @{ + 'global' = 'key' + } + } + } + } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; 'PendingCookies' = @{} } + 'Headers' = @{} + }; 'PendingCookies' = @{} + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -256,8 +277,9 @@ Describe 'Set-PodeCookie' { It 'Adds cookie to response with options' { $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; 'PendingCookies' = @{} } + 'Headers' = @{} + }; 'PendingCookies' = @{} + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -282,8 +304,9 @@ Describe 'Set-PodeCookie' { It 'Adds cookie to response with TTL' { $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; 'PendingCookies' = @{} } + 'Headers' = @{} + }; 'PendingCookies' = @{} + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -307,8 +330,9 @@ Describe 'Set-PodeCookie' { It 'Adds cookie to response with Expiry' { $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; 'PendingCookies' = @{} } + 'Headers' = @{} + }; 'PendingCookies' = @{} + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -336,11 +360,12 @@ Describe 'Update-PodeCookieExpiry' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{ - 'test' = @{ 'Name' = 'test'; 'Expires' = [datetime]::UtcNow } - } } + 'Headers' = @{} + } + 'PendingCookies' = @{ + 'test' = @{ 'Name' = 'test'; 'Expires' = [datetime]::UtcNow } + } + } Update-PodeCookieExpiry -Name 'test' -Duration 3600 ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should Be $true @@ -352,9 +377,10 @@ Describe 'Update-PodeCookieExpiry' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{} } + 'Headers' = @{} + } + 'PendingCookies' = @{} + } Update-PodeCookieExpiry -Name 'test' -Duration 3600 ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should Be $true @@ -364,11 +390,12 @@ Describe 'Update-PodeCookieExpiry' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{ - 'test' = @{ 'Name' = 'test'; 'Expires' = [datetime]::UtcNow } - } } + 'Headers' = @{} + } + 'PendingCookies' = @{ + 'test' = @{ 'Name' = 'test'; 'Expires' = [datetime]::UtcNow } + } + } Update-PodeCookieExpiry -Name 'test' -Expiry ([datetime]::UtcNow.AddDays(2)) ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddDays(1)) | Should Be $true @@ -380,11 +407,12 @@ Describe 'Update-PodeCookieExpiry' { $ttl = [datetime]::UtcNow $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{ - 'test' = @{ 'Name' = 'test'; 'Expires' = $ttl } - } } + 'Headers' = @{} + } + 'PendingCookies' = @{ + 'test' = @{ 'Name' = 'test'; 'Expires' = $ttl } + } + } Update-PodeCookieExpiry -Name 'test' $WebEvent.PendingCookies['test'].Expires | Should Be $ttl @@ -396,11 +424,12 @@ Describe 'Update-PodeCookieExpiry' { $ttl = [datetime]::UtcNow $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{ - 'test' = @{ 'Name' = 'test'; 'Expires' = $ttl } - } } + 'Headers' = @{} + } + 'PendingCookies' = @{ + 'test' = @{ 'Name' = 'test'; 'Expires' = $ttl } + } + } Update-PodeCookieExpiry -Name 'test' -Duration -1 $WebEvent.PendingCookies['test'].Expires | Should Be $ttl @@ -412,11 +441,12 @@ Describe 'Remove-PodeCookie' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } $WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{ - 'test' = @{ 'Name' = 'test'; 'Discard' = $false; 'Expires' = [datetime]::UtcNow } - } } + 'Headers' = @{} + } + 'PendingCookies' = @{ + 'test' = @{ 'Name' = 'test'; 'Discard' = $false; 'Expires' = [datetime]::UtcNow } + } + } Remove-PodeCookie -Name 'test' @@ -430,9 +460,10 @@ Describe 'Remove-PodeCookie' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } $WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - }; - 'PendingCookies' = @{} } + 'Headers' = @{} + } + 'PendingCookies' = @{} + } Remove-PodeCookie -Name 'test' diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 078d153ad..80f46599a 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -154,6 +154,9 @@ Describe 'Restart-PodeInternalServer' { Connections = [System.Collections.Concurrent.ConcurrentQueue[System.Net.Sockets.SocketAsyncEventArgs]]::new() } } + Http = @{ + Listener = $null + } OpenAPI = @{} BodyParsers = @{} AutoImport = @{ From 283727a6991f8b3495bdea112039ce6610f8e7ce Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sat, 23 Mar 2024 17:21:00 +0000 Subject: [PATCH 27/84] #1245: add some test util funcs, add SSE docs --- docs/Tutorials/Routes/Utilities/SSE.md | 259 +++++++++++++++++++++++++ src/Listener/PodeHttpRequest.cs | 4 +- src/Listener/PodeListener.cs | 18 ++ src/Listener/PodeResponse.cs | 4 +- src/Pode.psd1 | 4 + src/Private/Context.ps1 | 1 + src/Private/PodeServer.ps1 | 4 + src/Public/SSE.ps1 | 118 ++++++++++- 8 files changed, 403 insertions(+), 9 deletions(-) create mode 100644 docs/Tutorials/Routes/Utilities/SSE.md diff --git a/docs/Tutorials/Routes/Utilities/SSE.md b/docs/Tutorials/Routes/Utilities/SSE.md new file mode 100644 index 000000000..94af90fe1 --- /dev/null +++ b/docs/Tutorials/Routes/Utilities/SSE.md @@ -0,0 +1,259 @@ +# SSE + +You can convert regular HTTP requests made to a Route into an SSE connection, allowing you to stream events from your server to one or more connected clients. Connections can be scoped to just the Route that converted the request and it will be closed at the end of the Route like a normal request flow (Local), or you can keep the connection open beyond the request flow and be used server-wide for sending events (Global). + +SSE connections are typically made from client browsers via JavaScript, using the [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) class. + +## Convert Request + +To convert a request into an SSE connection use [`ConvertTo-PodeSseConnection`](../../../../Functions/SSE/ConvertTo-PodeSseConnection). This will automatically send back the appropriate HTTP response headers to the client, converting it into an SSE connection; allowing the connection to be kept open, and for events to be streamed back to the client. A `-Name` must be supplied during the conversion, allowing for easier reference to all connections later on, and allowing for different connection groups (of which, you can also have `-Group` within a Name as well). + +!!! important + For a request to be convertible, it must have an `Accept` HTTP request header value of `text/event-stream`. + +Any requests to the following Route will be converted to a globally scoped SSE connection, and be available under the `Events` name: + +```powershell +Add-PodeRoute -Method Get -Path '/events' -ScriptBlock { + ConvertTo-PodeSseConnection -Name 'Events' +} +``` + +You could then use [`Send-PodeSseEvent`](../../../../Functions/SSE/Send-PodeSseEvent) in a Schedule (more info [below](#send-events)) to broadcast an event, every minute, to all connected clients within the `Events` name: + +```powershell +Add-PodeSchedule -Name 'Example' -Cron (New-PodeCron -Every Minute) -ScriptBlock { + Send-PodeSseEvent -Name 'Events' -Data "Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" +} +``` + +Once [`ConvertTo-PodeSseConnection`](../../../../Functions/SSE/ConvertTo-PodeSseConnection) has been called, the `$WebEvent` object will be extended to include a new `SSE` property. This new property will have the following items: + +| Name | Description | +| ----------- | ------------------------------------------------------------------------------------------------------- | +| Name | The Name given to the connection | +| Group | An optional Group assigned to the connection within the Name | +| ClientId | The assigned ClientId for the connection - this will be different to a passed ClientId if using signing | +| LastEventId | The last EventId the client saw, if this is a reconnecting SSE request | +| IsLocal | Is the connection Local or Global | + +Therefore, after converting a request, you can get the client ID back via: + +```powershell +Add-PodeRoute -Method Get -Path '/events' -ScriptBlock { + ConvertTo-PodeSseConnection -Name 'Events' + $clientId = $WebEvent.Sse.ClientId +} +``` + +!!! tip + The Name, Group, and Client ID values are also sent back on the HTTP response during conversion as headers. These won't be available if you're using JavaScript's `EventSource` class, but could be if using other SSE libraries. The headers are: + + * `X-PODE-SSE-CLIENT-ID` + * `X-PODE-SSE-NAME` + * `X-PODE-SSE-GROUP` + +### ClientIds + +ClientIds created by [`ConvertTo-PodeSseConnection`](../../../../Functions/SSE/ConvertTo-PodeSseConnection) will be a GUID by default however, you can supply your own IDs via the `-ClientId` parameter: + +```powershell +Add-PodeRoute -Method Get -Path '/events' -ScriptBlock { + $clientId = Get-Random -Minimum 10000 -Maximum 999999 + ConvertTo-PodeSseConnection -Name 'Events' -ClientId $clientId +} +``` + +You can also [sign clientIds](#signing-clientids) as well. + +### Scopes + +The default scope for a new SSE connection is "Global", which means the connection will be stored internally and can be used outside of the converting Route to stream events back to the client. + +The default scope for new SSE connections can be altered by using [`Set-PodeSseDefaultScope`](../../../../Functions/SSE/Set-PodeSseDefaultScope). For example, if you wanted all new SSE connections to instead default to a Local scope: + +```powershell +Set-PodeSseDefaultScope -Scope Local +``` + +#### Global + +A Globally scoped SSE connection is the default (unless altered via [`Set-PodeSseDefaultScope`](../../../../Functions/SSE/Set-PodeSseDefaultScope)). A Global connection has the following features: + +* They are kept open, even after the Route that converted the request has finished. +* The connection is stored internally, so that events can be streamed to the clients from other Routes, Timers, etc. +* You can send events to a specific connection if you know the Name and ClientId for the connection. +* Global connections can be closed via [`Close-PodeSseConnection`](../../../../Functions/SSE/Close-PodeSseConnection). + +For example, the following will convert requests to `/events` into global SSE connections, and then a Schedule will send events to them every minute: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + + Add-PodeRoute -Method Get -Path '/events' -ScriptBlock { + ConvertTo-PodeSseConnection -Name 'Events' + } + + Add-PodeSchedule -Name 'Example' -Cron (New-PodeCron -Every Minute) -ScriptBlock { + Send-PodeSseEvent -Name 'Events' -Data "Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + } +} +``` + +#### Local + +A Local connection has the following features: + +* When the Route that converted the request has finished, the connection will be closed - the same as HTTP requests. +* The connection is **not** stored internally, it is only available for the lifecycle of the HTTP request. +* You can send events back to the connection from within the converting Route's scriptblock, but not from Timers, etc. When sending events back for local connections you will need to use the `-FromEvent` switch on [`Send-PodeSseEvent`](../../../../Functions/SSE/Send-PodeSseEvent). + +For example, the following will convert requests to `/events` into local SSE connections, and two events will be sent back to the client before the connection is closed: + +```powershell +Start-PodeServer { + Add-PodeEndpoint -Address * -Port 8090 -Protocol Http + + Add-PodeRoute -Method Get -Path '/events' -ScriptBlock { + ConvertTo-PodeSseConnection -Name 'Events' -Scope Local + Send-PodeSseEvent -FromEvent -Data "Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + Start-Sleep -Seconds 10 + Send-PodeSseEvent -FromEvent -Data "Hello there! The datetime is: $([datetime]::Now.TimeOfDay)" + } +} +``` + +### Inbuilt Events + +Pode has two inbuilt events that it will send to your SSE connections. These events will be sent automatically when a connection is opened, and when it is closed. + +!!! important + It is recommended to listen for the close event from Pode, as this way you'll know when Pode has closed the connection and you can perform any required clean-up. + +#### Open + +When an SSE connection is opened, via [`ConvertTo-PodeSseConnection`](../../../../Functions/SSE/ConvertTo-PodeSseConnection), Pode will send a `pode.open` event to your client. This event will also contain the `clientId`, `group`, and `name` of the SSE connection. + +You can listen for this event in JavaScript if using `EventSource`, as follows: + +```javascript +const sse = new EventSource('/events'); + +sse.addEventListener('pode.open', (e) => { + var data = JSON.parse(e.data); + let clientId = data.clientId; + let group = data.group; + let name = data.name; +}); +``` + +#### Close + +When an SSE connection is closed, either via [`Close-PodeSseConnection`](../../../../Functions/SSE/Close-PodeSseConnection) or when the Pode server stops, Pode will send a `pode.close` event to your clients. This will be an empty event, purely for clean-up purposes. + +You can listen for this event in JavaScript if using `EventSource`, as follows: + +```javascript +const sse = new EventSource('/events'); + +sse.addEventListener('pode.close', (e) => { + sse.close(); +}); +``` + +## Send Events + +To send an event from the server to one or more connected clients, you can use [`Send-PodeSseEvent`](../../../../Functions/SSE/Send-PodeSseEvent). Using the `-Data` parameter, you can either send a raw string value, or a more complex hashtable/psobject which will be auto-converted into a JSON string. + +For example, to broadcast an event to all clients on an "Events" SSE connection: + +```powershell +# simple string +Send-PodeSseEvent -Name 'Events' -Data 'Hello there!' + +# complex object +Send-PodeSseEvent -Name 'Events' -Data @{ Value = 'Hello there!' } +``` + +Or to send an event to a specific client: + +```powershell +Send-PodeSseEvent -Name 'Events' -ClientId 'some-client-id' -Data 'Hello there!' +``` + +You can also specify an optional `-Id` and `-EventType` for the SSE event being sent. The `-EventType` can be used in JavaScript to register event listeners, and the `-Id` is used by the browser to keep track of events being sent in case the connection is dropped. + +```powershell +$id = [int][datetime]::Now.TimeOfDay.TotalSeconds +$data = @{ Date = [datetime]::Now.ToString() } + +Send-PodeSseEvent -Name 'Events' -Id $id -EventType 'Date' -Data $data +``` + +### Broadcast Levels + +By default, Pode will allow broadcasting of events to all clients for an SSE connection Name, Group, or a specific ClientId. + +You can supply a custom broadcasting level for specific SSE connection names (or all), limiting broadcasting to requiring a specific ClientId for example, by using [`Set-PodeSseBroadcastLevel`](../../../../Functions/SSE/Set-PodeSseBroadcastLevel). If a `-Name` is not supplied then the level type is applied to all SSE connections. + +For example, the following will only allow events to be broadcast to an SSE connection name if a ClientId is also specified on [`Send-PodeSseEvent`](../../../../Functions/SSE/Send-PodeSseEvent): + +```powershell +# apply to all SSE connections +Set-PodeSseBroadcastLevel -Type 'ClientId' + +# apply to just SSE connections with name = Events +Set-PodeSseBroadcastLevel -Name 'Events' -Type 'ClientId' +``` + +The following levels are available: + +| Level | Description | +| -------- | ------------------------------------------------------------------- | +| Name | A Name is required. Groups/ClientIds are optional. | +| Group | A Name is required. One of either a Group and ClientId is required. | +| ClientId | A Name and a ClientId are required. | + +## Signing ClientIds + +Similar to Sessions and Cookies, you can sign SSE connection ClientIds. This can be done by calling [`Enable-PodeSseSigning`](../../../../Functions/SSE/Enable-PodeSseSigning) and supplying a `-Secret` to sign the ClientIds. + +!!! tip + You can use the inbuilt [`Get-PodeServerDefaultSecret`](../../../../Functions/Core/Get-PodeServerDefaultSecret) function to retrieve an internal Pode server secret which can be used. However, be warned that this secret is regenerated to a random value on every server start/restart. + +```powershell +Enable-PodeSseSigning -Secret 'super-secret' +Enable-PodeSseSigning -Secret (Get-PodeServerDefaultSecret) +``` + +When signing is enabled, all clientIds will be signed regardless if they're an internally generated random GUID or supplied via `-ClientId` on [`ConvertTo-PodeSseConnection`](../../../../Functions/SSE/ConvertTo-PodeSseConnection). A signed clientId will look as follows, and have the structure `s:.`: + +```plain +s:5d12f974-7b1a-4524-ab93-6afbf42c4ffa.uvG49LcojTMuJ0l4yzBzr6jCqEV8gGC/0YgsYU1QEuQ= +``` + +You can also supply the `-Strict` switch to [`Enable-PodeSseSigning`](../../../../Functions/SSE/Enable-PodeSseSigning), which will extend the secret during signing with the client's IP Address and User Agent. + +## Request Headers + +If you have an SSE connection open for a client, and you want to have the client send AJAX requests to the server but have the responses streamed back over the SSE connection, then you can identify the SSE connection for the client using the following HTTP headers: + +* `X-PODE-SSE-CLIENT-ID` +* `X-PODE-SSE-NAME` +* `X-PODE-SSE-GROUP` + +At a minimum, you'll need the `X-PODE-SSE-CLIENT-ID` header. If supplied Pode will automatically verify the client ID for you, including if the signing of the client ID is valid - if you're using client ID signing. + +When these headers are supplied in a request, Pode will set up the `$WebEvent.Sse` property again - similar to the property set up from [conversion](#convert-request) above: + +| Name | Description | +| ----------- | ------------------------------------------------------------------ | +| Name | The Name for the connection from X-PODE-SSE-NAME | +| Group | The Group for the connection from X-PODE-SSE-GROUP | +| ClientId | The assigned ClientId for the connection from X-PODE-SSE-CLIENT-ID | +| LastEventId | `$null` | +| IsLocal | `$false` | + +!!! note + If you only supply the Name or Group headers, then the `$WebEvent.Sse` property will not be configured. The ClientId is required as a minimum. diff --git a/src/Listener/PodeHttpRequest.cs b/src/Listener/PodeHttpRequest.cs index 42a156717..54b6ffbd4 100644 --- a/src/Listener/PodeHttpRequest.cs +++ b/src/Listener/PodeHttpRequest.cs @@ -304,8 +304,8 @@ private int ParseHeaders(string[] reqLines, string newline) SseClientId = $"{Headers["X-Pode-Sse-Client-Id"]}"; if (HasSseClientId) { - SseClientName = $"{Headers["X-Pode-Sse-Client-Name"]}"; - SseClientGroup = $"{Headers["X-Pode-Sse-Client-Group"]}"; + SseClientName = $"{Headers["X-Pode-Sse-Name"]}"; + SseClientGroup = $"{Headers["X-Pode-Sse-Group"]}"; } // keep-alive? diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index 5761b7826..6821a3e08 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -191,6 +191,24 @@ public void CloseSseConnection(string name, string[] groups, string[] clientIds) }, CancellationToken); } + public bool TestSseConnectionExists(string name, string clientId) + { + // check name + if (!ServerEvents.ContainsKey(name)) + { + return false; + } + + // check clientId + if (!string.IsNullOrEmpty(clientId) && !ServerEvents[name].ContainsKey(clientId)) + { + return false; + } + + // exists + return true; + } + public PodeServerSignal GetServerSignal(CancellationToken cancellationToken = default(CancellationToken)) { return ServerSignals.Get(cancellationToken); diff --git a/src/Listener/PodeResponse.cs b/src/Listener/PodeResponse.cs index a54e082b4..3c42a9865 100644 --- a/src/Listener/PodeResponse.cs +++ b/src/Listener/PodeResponse.cs @@ -223,11 +223,11 @@ public string SetSseConnection(PodeSseScope scope, string clientId, string name, } Headers.Set("X-Pode-Sse-Client-Id", clientId); - Headers.Set("X-Pode-Sse-Client-Name", name); + Headers.Set("X-Pode-Sse-Name", name); if (!string.IsNullOrEmpty(group)) { - Headers.Set("X-Pode-Sse-Client-Group", group); + Headers.Set("X-Pode-Sse-Group", group); } // send headers, and open event diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 9af1a5eea..72fef9247 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -102,6 +102,10 @@ 'Set-PodeSseBroadcastLevel', 'Get-PodeSseBroadcastLevel', 'Test-PodeSseBroadcastLevel', + 'Set-PodeSseDefaultScope', + 'Get-PodeSseDefaultScope', + 'Test-PodeSseName', + 'Test-PodeSseClientId', # utility helpers 'Close-PodeDisposable', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 8b85e90ef..7643e0850 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -162,6 +162,7 @@ function New-PodeContext { Signed = $false Secret = $null Strict = $false + DefaultScope = 'Global' BroadcastLevel = @{} } diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index dbebe163f..40596694e 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -180,6 +180,10 @@ function Start-PodeWebServer { throw [System.Net.Http.HttpRequestException]::new("The X-PODE-SSE-CLIENT-ID value is not valid: $($WebEvent.Request.SseClientId)") } + if (![string]::IsNullOrEmpty($WebEvent.Request.SseClientName) -and !(Test-PodeSseClientId -Name $WebEvent.Request.SseClientName -ClientId $WebEvent.Request.SseClientId)) { + throw [System.Net.Http.HttpRequestException]::new("The SSE Connection being referenced via the X-PODE-SSE-NAME and X-PODE-SSE-CLIENT-ID headers does not exist: [$($WebEvent.Request.SseClientName)] $($WebEvent.Request.SseClientId)") + } + $WebEvent.Sse = @{ Name = $WebEvent.Request.SseClientName Group = $WebEvent.Request.SseClientGroup diff --git a/src/Public/SSE.ps1 b/src/Public/SSE.ps1 index bd2f6de68..c3203e8a4 100644 --- a/src/Public/SSE.ps1 +++ b/src/Public/SSE.ps1 @@ -13,7 +13,8 @@ The Name of the SSE connection, which ClientIds will be stored under. An optional Group for this SSE connection, to enable broadcasting events to all connections for an SSE connection name in a Group. .PARAMETER Scope -The Scope of the SSE connection, either Local or Global (Default: Global). +The Scope of the SSE connection, either Default, Local or Global (Default: Default). +- If the Scope is Default, then it will be Global unless the default has been updated via Set-PodeSseDefaultScope. - If the Scope is Local, then the SSE connection will only be opened for the duration of the request to a Route that configured it. - If the Scope is Global, then the SSE connection will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc. @@ -56,9 +57,9 @@ function ConvertTo-PodeSseConnection { $Group, [Parameter()] - [ValidateSet('Local', 'Global')] + [ValidateSet('Default', 'Local', 'Global')] [string] - $Scope = 'Global', + $Scope = 'Default', [Parameter()] [int] @@ -80,6 +81,11 @@ function ConvertTo-PodeSseConnection { throw 'SSE can only be configured on requests with an Accept header value of text/event-stream' } + # check for default scope, and set + if ($Scope -ieq 'default') { + $Scope = $PodeContext.Server.Sse.DefaultScope + } + # generate clientId $ClientId = New-PodeSseClientId -ClientId $ClientId @@ -96,6 +102,53 @@ function ConvertTo-PodeSseConnection { } } +<# +.SYNOPSIS +Sets the default scope for new SSE connections. + +.DESCRIPTION +Sets the default scope for new SSE connections. + +.PARAMETER Scope +The default Scope for new SSE connections, either Local or Global. +- If the Scope is Local, then new SSE connections will only be opened for the duration of the request to a Route that configured it. +- If the Scope is Global, then new SSE connections will be cached internally so events can be sent to the connection from Tasks, Timers, and other Routes, etc. + +.EXAMPLE +Set-PodeSseDefaultScope -Scope Local + +.EXAMPLE +Set-PodeSseDefaultScope -Scope Global +#> +function Set-PodeSseDefaultScope { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [ValidateSet('Local', 'Global')] + [string] + $Scope + ) + + $PodeContext.Server.Sse.DefaultScope = $Scope +} + +<# +.SYNOPSIS +Retrieves the default SSE connection scope for new SSE connections. + +.DESCRIPTION +Retrieves the default SSE connection scope for new SSE connections. + +.EXAMPLE +$scope = Get-PodeSseDefaultScope +#> +function Get-PodeSseDefaultScope { + [CmdletBinding()] + param() + + return $PodeContext.Server.Sse.DefaultScope +} + <# .SYNOPSIS Send an Event to one or more SSE connections. @@ -129,7 +182,7 @@ The Depth to generate the JSON document - the larger this value the worse perfor .PARAMETER FromEvent If supplied, the SSE connection Name and ClientId will atttempt to be retrived from $WebEvent.Sse. -These details will be set if ConvertTo-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-CLIENT-NAME are set on an HTTP request. +These details will be set if ConvertTo-PodeSseConnection has just been called. Or if X-PODE-SSE-CLIENT-ID and X-PODE-SSE-NAME are set on an HTTP request. .EXAMPLE Send-PodeSseEvent -FromEvent -Data 'This is an event' @@ -147,7 +200,7 @@ Send-PodeSseEvent -Name 'Actions' -Group 'admins' -Data @{ Message = 'A message' Send-PodeSseEvent -Name 'Actions' -Data @{ Message = 'A message' } -ID 123 -EventType 'action' #> function Send-PodeSseEvent { - [CmdletBinding(DefaultParameterSetName = 'Name')] + [CmdletBinding()] param( [Parameter(Mandatory = $true, ParameterSetName = 'Name')] [string] @@ -345,6 +398,61 @@ function Test-PodeSseClientIdValid { return Test-PodeSseClientIdSigned -ClientId $ClientId } +<# +.SYNOPSIS +Test if the name of an SSE connection exists or not. + +.DESCRIPTION +Test if the name of an SSE connection exists or not. + +.PARAMETER Name +The Name of an SSE connection to test. + +.EXAMPLE +if (Test-PodeSseName -Name 'Example') { ... } +#> +function Test-PodeSseName { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name + ) + + return $PodeContext.Server.Http.Listener.TestSseConnectionExists($Name) +} + +<# +.SYNOPSIS +Test if an SSE connection ClientId exists or not. + +.DESCRIPTION +Test if an SSE connection ClientId exists or not. + +.PARAMETER Name +The Name of an SSE connection. + +.PARAMETER ClientId +The SSE connection ClientId to test. + +.EXAMPLE +if (Test-PodeSseClientId -Name 'Example' -ClientId 'my-client-id') { ... } +#> +function Test-PodeSseClientId { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [string] + $ClientId + ) + + return $PodeContext.Server.Http.Listener.TestSseConnectionExists($Name, $ClientId) +} + <# .SYNOPSIS Generate a new SSE connection ClientId. From 2338dece2c9d7eccf081a1465271f765a041042c Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sat, 23 Mar 2024 19:01:05 +0000 Subject: [PATCH 28/84] #1245: minor comments tweak --- src/Public/Responses.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index db5fae517..8fff8c91a 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -1582,8 +1582,16 @@ function Add-PodeViewFolder { $PodeContext.Server.Views[$Name] = $Source } -#TODO: flag that this is a dangerous function, which will force send a response before the end of a route -# only use if you know what you're doing! +<# +.SYNOPSIS +Pre-emptively send an HTTP response back to the client. This can be dangerous, so only use this function if you know what you're doing. + +.DESCRIPTION +Pre-emptively send an HTTP response back to the client. This can be dangerous, so only use this function if you know what you're doing. + +.EXAMPLE +Send-PodeResponse +#> function Send-PodeResponse { [CmdletBinding()] param() From f7a554203ec2b6b44315cd1cbc41a6220d85af4f Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Sun, 24 Mar 2024 16:56:29 +0000 Subject: [PATCH 29/84] #1251: initial work for enabling dualmode on endpoints --- Pode.sln | 30 ++++++ examples/rest-api.ps1 | 2 +- src/Listener/PodeContext.cs | 11 ++- src/Listener/PodeEndpoint.cs | 78 ++++++++++++++++ src/Listener/PodeFileWatcher.cs | 2 +- src/Listener/PodeListener.cs | 6 +- src/Listener/PodeSmtpRequest.cs | 2 +- src/Listener/PodeSocket.cs | 106 +++++++++++++++------- src/Private/Context.ps1 | 5 - src/Private/Endpoints.ps1 | 4 +- src/Private/Helpers.ps1 | 63 +++++++++++-- src/Private/PodeServer.ps1 | 44 +++++---- src/Private/Security.ps1 | 19 ---- src/Private/Server.ps1 | 19 ++-- src/Private/SmtpServer.ps1 | 26 ++++-- src/Private/TcpServer.ps1 | 26 ++++-- src/Public/Core.ps1 | 14 ++- src/Public/Routes.ps1 | 12 --- src/Public/Verbs.ps1 | 4 - tests/unit/Helpers.Tests.ps1 | 149 +++++++++++++++--------------- tests/unit/Routes.Tests.ps1 | 156 +++++++++++++++++--------------- 21 files changed, 495 insertions(+), 283 deletions(-) create mode 100644 Pode.sln create mode 100644 src/Listener/PodeEndpoint.cs diff --git a/Pode.sln b/Pode.sln new file mode 100644 index 000000000..66eb3805a --- /dev/null +++ b/Pode.sln @@ -0,0 +1,30 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{41F81369-8680-4BC5-BA16-C7891D245717}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pode", "src\Listener\Pode.csproj", "{772D5C9F-1B25-46A7-8977-412A5F7F77D1}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {772D5C9F-1B25-46A7-8977-412A5F7F77D1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {772D5C9F-1B25-46A7-8977-412A5F7F77D1} = {41F81369-8680-4BC5-BA16-C7891D245717} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F24001DC-2986-4305-B1B5-8E73BCDF1A77} + EndGlobalSection +EndGlobal diff --git a/examples/rest-api.ps1 b/examples/rest-api.ps1 index 1f0793544..0c16c2c53 100644 --- a/examples/rest-api.ps1 +++ b/examples/rest-api.ps1 @@ -7,7 +7,7 @@ Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop # create a server, and start listening on port 8086 Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8086 -Protocol Http + Add-PodeEndpoint -Address 'localhost' -Port 8086 -Protocol Http -DualMode # request logging New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging diff --git a/src/Listener/PodeContext.cs b/src/Listener/PodeContext.cs index ce4ceb38c..ebc00fdca 100644 --- a/src/Listener/PodeContext.cs +++ b/src/Listener/PodeContext.cs @@ -15,10 +15,15 @@ public class PodeContext : PodeProtocol, IDisposable public PodeResponse Response { get; private set; } public PodeListener Listener { get; private set; } public Socket Socket { get; private set; } - public PodeSocket PodeSocket { get; private set;} + public PodeSocket PodeSocket { get; private set; } public DateTime Timestamp { get; private set; } public Hashtable Data { get; private set; } + public string EndpointName + { + get => PodeSocket.Name; + } + private object _lockable = new object(); private PodeContextState _state; @@ -281,7 +286,7 @@ public async void Receive() SetContextType(); EndReceive(close); } - catch (OperationCanceledException) {} + catch (OperationCanceledException) { } } catch (Exception ex) { @@ -436,7 +441,7 @@ public void Dispose(bool force) Response.Dispose(); } } - catch {} + catch { } // if keep-alive, or awaiting body, setup for re-receive if ((_awaitingBody || (IsKeepAlive && !IsErrored && !IsTimeout && !Response.SseEnabled)) && !force) diff --git a/src/Listener/PodeEndpoint.cs b/src/Listener/PodeEndpoint.cs new file mode 100644 index 000000000..917dfb643 --- /dev/null +++ b/src/Listener/PodeEndpoint.cs @@ -0,0 +1,78 @@ +using System; +using System.Net; +using System.Net.Sockets; + +namespace Pode +{ + public class PodeEndpoint : IDisposable + { + public IPAddress IPAddress { get; private set; } + public int Port { get; private set; } + public IPEndPoint Endpoint { get; private set; } + public Socket Socket { get; private set; } + public PodeSocket PodeSocket { get; private set; } + public bool DualMode { get; private set; } + public bool IsDisposed { get; private set; } + + public int ReceiveTimeout + { + get => Socket.ReceiveTimeout; + set => Socket.ReceiveTimeout = value; + } + + public PodeEndpoint(PodeSocket socket, IPAddress ipAddress, int port, bool dualMode) + { + IsDisposed = false; + PodeSocket = socket; + IPAddress = ipAddress; + Port = port; + Endpoint = new IPEndPoint(ipAddress, port); + + Socket = new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp) + { + ReceiveTimeout = 100, + NoDelay = true + }; + + if (dualMode && Endpoint.AddressFamily == AddressFamily.InterNetworkV6) + { + DualMode = true; + Socket.DualMode = true; + Socket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false); + } + else + { + DualMode = false; + } + } + + public void Listen() + { + Socket.Bind(Endpoint); + Socket.Listen(int.MaxValue); + } + + public bool AcceptAsync(SocketAsyncEventArgs args) + { + if (IsDisposed) + { + throw new ObjectDisposedException("PodeEndpoint disposed"); + } + + return Socket.AcceptAsync(args); + } + + public void Dispose() + { + IsDisposed = true; + PodeSocket.CloseSocket(Socket); + Socket = default(Socket); + } + + public new bool Equals(object obj) + { + var _endpoint = (PodeEndpoint)obj; + return Endpoint.ToString() == _endpoint.Endpoint.ToString() && Port == _endpoint.Port; + } + } +} \ No newline at end of file diff --git a/src/Listener/PodeFileWatcher.cs b/src/Listener/PodeFileWatcher.cs index 9a2d2f1e7..da1249cdb 100644 --- a/src/Listener/PodeFileWatcher.cs +++ b/src/Listener/PodeFileWatcher.cs @@ -10,7 +10,7 @@ public class PodeFileWatcher private RecoveringFileSystemWatcher FileWatcher; public string Name { get; private set; } - public ISet EventsRegistered {get; private set; } + public ISet EventsRegistered { get; private set; } public PodeFileWatcher(string name, string path, bool includeSubdirectories, int internalBufferSize, NotifyFilters notifyFilters) { diff --git a/src/Listener/PodeListener.cs b/src/Listener/PodeListener.cs index 6821a3e08..f5a1dc655 100644 --- a/src/Listener/PodeListener.cs +++ b/src/Listener/PodeListener.cs @@ -137,7 +137,8 @@ public void AddSseConnection(PodeServerEvent sse) public void SendSseEvent(string name, string[] groups, string[] clientIds, string eventType, string data, string id = null) { - Task.Factory.StartNew(() => { + Task.Factory.StartNew(() => + { if (!ServerEvents.ContainsKey(name)) { return; @@ -165,7 +166,8 @@ public void SendSseEvent(string name, string[] groups, string[] clientIds, strin public void CloseSseConnection(string name, string[] groups, string[] clientIds) { - Task.Factory.StartNew(() => { + Task.Factory.StartNew(() => + { if (!ServerEvents.ContainsKey(name)) { return; diff --git a/src/Listener/PodeSmtpRequest.cs b/src/Listener/PodeSmtpRequest.cs index a2b49eadc..8039d45f3 100644 --- a/src/Listener/PodeSmtpRequest.cs +++ b/src/Listener/PodeSmtpRequest.cs @@ -398,7 +398,7 @@ private void ParseBoundary() var contentType = $"{headers["Content-Type"]}"; var contentEncoding = $"{headers["Content-Transfer-Encoding"]}"; - // get the boundary + // get the boundary var body = ParseBody(boundaryBody, Boundary); var bodyBytes = ConvertBodyEncoding(body, contentEncoding); diff --git a/src/Listener/PodeSocket.cs b/src/Listener/PodeSocket.cs index ac2588153..8f8b5fd7d 100644 --- a/src/Listener/PodeSocket.cs +++ b/src/Listener/PodeSocket.cs @@ -13,17 +13,16 @@ namespace Pode { public class PodeSocket : PodeProtocol, IDisposable { - public IPAddress IPAddress { get; private set; } + public string Name { get; private set; } public List Hostnames { get; private set; } - public int Port { get; private set; } - public IPEndPoint Endpoint { get; private set; } + public IList Endpoints { get; private set; } public X509Certificate Certificate { get; private set; } public bool AllowClientCertificate { get; private set; } public SslProtocols Protocols { get; private set; } public PodeTlsMode TlsMode { get; private set; } - public Socket Socket { get; private set; } public string AcknowledgeMessage { get; set; } public bool CRLFMessageEnd { get; set; } + public bool DualMode { get; private set; } private ConcurrentQueue AcceptConnections; private ConcurrentQueue ReceiveConnections; @@ -33,41 +32,50 @@ public class PodeSocket : PodeProtocol, IDisposable public bool IsSsl { - get => (Certificate != default(X509Certificate)); + get => Certificate != default(X509Certificate); } + private int _receiveTimeout; public int ReceiveTimeout { - get => Socket.ReceiveTimeout; - set => Socket.ReceiveTimeout = value; + get => _receiveTimeout; + set + { + _receiveTimeout = value; + foreach (var ep in Endpoints) + { + ep.ReceiveTimeout = value; + } + } } public bool HasHostnames => Hostnames.Any(); public string Hostname { - get => (HasHostnames ? Hostnames[0] : IPAddress.ToString()); + get => HasHostnames ? Hostnames[0] : Endpoints[0].IPAddress.ToString(); } - public PodeSocket(IPAddress ipAddress, int port, SslProtocols protocols, PodeProtocolType type, X509Certificate certificate = null, bool allowClientCertificate = false, PodeTlsMode tlsMode = PodeTlsMode.Implicit) + public PodeSocket(string name, IPAddress[] ipAddress, int port, SslProtocols protocols, PodeProtocolType type, X509Certificate certificate = null, bool allowClientCertificate = false, PodeTlsMode tlsMode = PodeTlsMode.Implicit, bool dualMode = false) : base(type) { - IPAddress = ipAddress; - Port = port; + Name = name; Certificate = certificate; AllowClientCertificate = allowClientCertificate; TlsMode = tlsMode; Protocols = protocols; Hostnames = new List(); - Endpoint = new IPEndPoint(ipAddress, port); + DualMode = dualMode; AcceptConnections = new ConcurrentQueue(); ReceiveConnections = new ConcurrentQueue(); PendingSockets = new Dictionary(); + Endpoints = new List(); - Socket = new Socket(Endpoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp); - Socket.ReceiveTimeout = 100; - Socket.NoDelay = true; + foreach (var addr in ipAddress) + { + Endpoints.Add(new PodeEndpoint(this, addr, port, dualMode)); + } } public void BindListener(PodeListener listener) @@ -77,25 +85,39 @@ public void BindListener(PodeListener listener) public void Listen() { - Socket.Bind(Endpoint); - Socket.Listen(int.MaxValue); + foreach (var ep in Endpoints) + { + ep.Listen(); + } } public void Start() { - var args = default(SocketAsyncEventArgs); - if (!AcceptConnections.TryDequeue(out args)) + foreach (var ep in Endpoints) + { + StartEndpoint(ep); + } + } + + private void StartEndpoint(PodeEndpoint endpoint) + { + if (endpoint.IsDisposed) + { + return; + } + + if (!AcceptConnections.TryDequeue(out SocketAsyncEventArgs args)) { args = NewAcceptConnection(); } - args.AcceptSocket = default(Socket); - args.UserToken = this; - var raised = false; + args.AcceptSocket = default; + args.UserToken = endpoint; + bool raised; try { - raised = Socket.AcceptAsync(args); + raised = endpoint.AcceptAsync(args); } catch (ObjectDisposedException) { @@ -131,7 +153,7 @@ public void StartReceive(PodeContext context) private void StartReceive(SocketAsyncEventArgs args) { args.SetBuffer(new byte[0], 0, 0); - var raised = false; + bool raised; try { @@ -158,11 +180,11 @@ private void ProcessAccept(SocketAsyncEventArgs args) { // get details var accepted = args.AcceptSocket; - var socket = (PodeSocket)args.UserToken; + var endpoint = (PodeEndpoint)args.UserToken; var error = args.SocketError; // start the socket again - socket.Start(); + StartEndpoint(endpoint); // close socket if not successful, or if listener is stopped - close now! if ((accepted == default(Socket)) || (error != SocketError.Success) || (!Listener.IsConnected)) @@ -229,8 +251,8 @@ private void ProcessReceive(SocketAsyncEventArgs args) context.RenewTimeoutToken(); Task.Factory.StartNew(() => context.Receive(), context.ContextTimeoutToken.Token); } - catch (OperationCanceledException) {} - catch (IOException) {} + catch (OperationCanceledException) { } + catch (IOException) { } catch (AggregateException aex) { PodeHelpers.HandleAggregateException(aex, Listener, PodeLoggingLevel.Error, true); @@ -340,9 +362,7 @@ private SocketAsyncEventArgs NewReceiveConnection() private SocketAsyncEventArgs GetReceiveConnection() { - var args = default(SocketAsyncEventArgs); - - if (!ReceiveConnections.TryDequeue(out args)) + if (!ReceiveConnections.TryDequeue(out SocketAsyncEventArgs args)) { args = NewReceiveConnection(); } @@ -397,7 +417,13 @@ public bool CheckHostname(string hostname) public void Dispose() { - CloseSocket(Socket); + // close endpoints + foreach (var ep in Endpoints) + { + ep.Dispose(); + } + + Endpoints.Clear(); // close receiving contexts/sockets try @@ -407,6 +433,8 @@ public void Dispose() { CloseSocket(_sockets[i]); } + + PendingSockets.Clear(); } catch (Exception ex) { @@ -436,7 +464,6 @@ public static void CloseSocket(Socket socket) // dispose socket.Close(); socket.Dispose(); - socket = default(Socket); } private void ClearSocketAsyncEvent(SocketAsyncEventArgs e) @@ -448,7 +475,18 @@ private void ClearSocketAsyncEvent(SocketAsyncEventArgs e) public new bool Equals(object obj) { var _socket = (PodeSocket)obj; - return (Endpoint.ToString() == _socket.Endpoint.ToString() && Port == _socket.Port); + foreach (var ep in Endpoints) + { + foreach (var _oEp in _socket.Endpoints) + { + if (!ep.Equals(_oEp)) + { + return false; + } + } + } + + return true; } } } \ No newline at end of file diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 7643e0850..c7dcbeb22 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -250,11 +250,6 @@ function New-PodeContext { # set the IP address details $ctx.Server.Endpoints = @{} $ctx.Server.EndpointsMap = @{} - $ctx.Server.FindEndpoints = @{ - Route = $false - Smtp = $false - Tcp = $false - } # general encoding for the server $ctx.Server.Encoding = New-Object System.Text.UTF8Encoding diff --git a/src/Private/Endpoints.ps1 b/src/Private/Endpoints.ps1 index 61e2f1e15..0e0f6c3c7 100644 --- a/src/Private/Endpoints.ps1 +++ b/src/Private/Endpoints.ps1 @@ -216,7 +216,7 @@ function Find-PodeEndpointName { # change localhost/computer name to ip address if (($Address -ilike 'localhost:*') -or ($Address -ilike "$($PodeContext.Server.ComputerName):*")) { - $Address = ($Address -ireplace "(localhost|$([regex]::Escape($PodeContext.Server.ComputerName)))\:", "(127\.0\.0\.1|0\.0\.0\.0|localhost|$([regex]::Escape($PodeContext.Server.ComputerName))):") + $Address = ($Address -ireplace "(localhost|$([regex]::Escape($PodeContext.Server.ComputerName)))\:", "(127\.0\.0\.1|0\.0\.0\.0|\:\:ffff\:127\.0\.0\.1|\:\:ffff\:0\:0|\[\:\:\]|\[\:\:1\]|\:\:1|\:\:|localhost|$([regex]::Escape($PodeContext.Server.ComputerName))):") } else { $Address = [regex]::Escape($Address) @@ -265,7 +265,7 @@ function Find-PodeEndpointName { #> # set * address as string - $_anyAddress = "0\.0\.0\.0:$($LocalAddress.Port)" + $_anyAddress = "(0\.0\.0\.0|\[\:\:\]|\:\:|\:\:ffff\:0\:0):$($LocalAddress.Port)" $key = "$($Protocol)\|$($_anyAddress)" # try and find endpoint for any address diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index ecc38dc86..ab727c264 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -148,7 +148,7 @@ function Get-PodeHostIPRegex { $Type ) - $ip_rgx = '\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all' + $ip_rgx = '\[?([a-f0-9]*\:){1,}[a-f0-9]*((\d+\.){3}\d+)?\]?|((\d+\.){3}\d+)|\*|all' $host_rgx = '([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+' switch ($Type.ToLowerInvariant()) { @@ -232,7 +232,7 @@ function Test-PodeIPAddress { $IPOnly ) - if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -ieq '*') -or ($IP -ieq 'all')) { + if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) { return $true } @@ -322,7 +322,7 @@ function Test-PodeIPAddressLocal { $IP ) - return (@('127.0.0.1', '::1', '[::1]', 'localhost') -icontains $IP) + return (@('127.0.0.1', '::1', '[::1]', '::ffff:127.0.0.1', 'localhost') -icontains $IP) } function Test-PodeIPAddressAny { @@ -345,20 +345,62 @@ function Test-PodeIPAddressLocalOrAny { return ((Test-PodeIPAddressLocal -IP $IP) -or (Test-PodeIPAddressAny -IP $IP)) } +function Resolve-PodeIPDualMode { + param( + [Parameter()] + [ipaddress] + $IP + ) + + # do nothing if IPv6Any + if ($IP -eq [ipaddress]::IPv6Any) { + return $IP + } + + # check loopbacks + if ($IP -eq [ipaddress]::Loopback) { + return @($IP, [ipaddress]::IPv6Loopback) + } + + if ($IP -eq [ipaddress]::IPv6Loopback) { + return @($IP, [ipaddress]::Loopback) + } + + # if iIPv4, convert and return both + if ($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { + return @($IP, $IP.MapToIPv6()) + } + + # if IPv6, only convert if valid IPv4 + if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6) -and $IP.IsIPv4MappedToIPv6) { + return @($IP, $IP.MapToIPv4()) + } + + # just return the IP + return $IP +} + function Get-PodeIPAddress { param( [Parameter()] [string] - $IP + $IP, + + [switch] + $DualMode ) - # any address for IPv4 - if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -ieq '*') -or ($IP -ieq 'all')) { + # any address for IPv4 (or IPv6 for DualMode) + if ([string]::IsNullOrWhiteSpace($IP) -or ($IP -iin @('*', 'all'))) { + if ($DualMode) { + return [System.Net.IPAddress]::IPv6Any + } + return [System.Net.IPAddress]::Any } - # any address for IPv6 - if (($IP -ieq '::') -or ($IP -ieq '[::]')) { + # any address for IPv6 explicitly + if ($IP -iin @('::', '[::]')) { return [System.Net.IPAddress]::IPv6Any } @@ -367,6 +409,11 @@ function Get-PodeIPAddress { return [System.Net.IPAddress]::Loopback } + # localhost IPv6 explicitly + if ($IP -iin @('[::1]', '::1')) { + return [System.Net.IPAddress]::IPv6Loopback + } + # hostname if ($IP -imatch "^$(Get-PodeHostIPRegex -Type Hostname)$") { return $IP diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 40596694e..537527b10 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -27,13 +27,20 @@ function Start-PodeWebServer { @(Get-PodeEndpoints -Type Http, Ws) | ForEach-Object { # get the ip address $_ip = [string]($_.Address) - $_ip = (Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1) - $_ip = (Get-PodeIPAddress $_ip) + $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 + $_ip = Get-PodeIPAddress -IP $_ip -DualMode:($_.DualMode) + + # dual mode? + $addrs = $_ip + if ($_.DualMode) { + $addrs = Resolve-PodeIPDualMode -IP $_ip + } # the endpoint $_endpoint = @{ + Name = $_.Name Key = "$($_ip):$($_.Port)" - Address = $_ip + Address = $addrs Hostname = $_.HostName IsIPAddress = $_.IsIPAddress Port = $_.Port @@ -44,6 +51,7 @@ function Start-PodeWebServer { Type = $_.Type Pool = $_.Runspace.PoolName SslProtocols = $_.Ssl.Protocols + DualMode = $_.DualMode } # add endpoint to list @@ -71,7 +79,7 @@ function Start-PodeWebServer { try { # register endpoints on the listener $endpoints | ForEach-Object { - $socket = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)ListenerSocket -Address `$_.Address -Port `$_.Port -SslProtocols `$_.SslProtocols -Type `$endpointsMap[`$_.Key].Type -Certificate `$_.Certificate -AllowClientCertificate `$_.AllowClientCertificate"))) + $socket = (. ([scriptblock]::Create("New-Pode$($PodeContext.Server.ListenerType)ListenerSocket -Name `$_.Name -Address `$_.Address -Port `$_.Port -SslProtocols `$_.SslProtocols -Type `$endpointsMap[`$_.Key].Type -Certificate `$_.Certificate -AllowClientCertificate `$_.AllowClientCertificate -DualMode:`$_.DualMode"))) $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout if (!$_.IsIPAddress) { @@ -130,7 +138,7 @@ function Start-PodeWebServer { Endpoint = @{ Protocol = $Request.Url.Scheme Address = $Request.Host - Name = $null + Name = $context.EndpointName } ContentType = $Request.ContentType ErrorType = $null @@ -163,9 +171,6 @@ function Start-PodeWebServer { $WebEvent.AcceptEncoding = (Get-PodeAcceptEncoding -AcceptEncoding (Get-PodeHeader -Name 'Accept-Encoding') -ThrowError) $WebEvent.Ranges = (Get-PodeRanges -Range (Get-PodeHeader -Name 'Range') -ThrowError) - # endpoint name - $WebEvent.Endpoint.Name = (Find-PodeEndpointName -Protocol $WebEvent.Endpoint.Protocol -Address $WebEvent.Endpoint.Address -LocalAddress $WebEvent.Request.LocalEndPoint -Enabled:($PodeContext.Server.FindEndpoints.Route)) - # add logging endware for post-request Add-PodeRequestLogEndware -WebEvent $WebEvent @@ -379,7 +384,7 @@ function Start-PodeWebServer { Endpoint = @{ Protocol = $Request.Url.Scheme Address = $Request.Host - Name = $null + Name = $context.Signal.Context.EndpointName } Route = $null ClientId = $context.Signal.ClientId @@ -388,9 +393,6 @@ function Start-PodeWebServer { Metadata = @{} } - # endpoint name - $SignalEvent.Endpoint.Name = (Find-PodeEndpointName -Protocol $SignalEvent.Endpoint.Protocol -Address $SignalEvent.Endpoint.Address -LocalAddress $SignalEvent.Request.LocalEndPoint -Enabled:($PodeContext.Server.FindEndpoints.Route)) - # see if we have a route and invoke it, otherwise auto-send $SignalEvent.Route = Find-PodeSignalRoute -Path $SignalEvent.Path -EndpointName $SignalEvent.Endpoint.Name @@ -464,8 +466,9 @@ function Start-PodeWebServer { return @(foreach ($endpoint in $endpoints) { @{ - Url = $endpoint.Url - Pool = $endpoint.Pool + Url = $endpoint.Url + Pool = $endpoint.Pool + DualMode = $endpoint.DualMode } }) } @@ -485,7 +488,11 @@ function New-PodeListenerSocket { [CmdletBinding()] param( [Parameter(Mandatory = $true)] - [ipaddress] + [string] + $Name, + + [Parameter(Mandatory = $true)] + [ipaddress[]] $Address, [Parameter(Mandatory = $true)] @@ -506,8 +513,11 @@ function New-PodeListenerSocket { [Parameter()] [bool] - $AllowClientCertificate + $AllowClientCertificate, + + [switch] + $DualMode ) - return [PodeSocket]::new($Address, $Port, $SslProtocols, $Type, $Certificate, $AllowClientCertificate) + return [PodeSocket]::new($Name, $Address, $Port, $SslProtocols, $Type, $Certificate, $AllowClientCertificate, 'Implicit', $DualMode.IsPresent) } \ No newline at end of file diff --git a/src/Private/Security.ps1 b/src/Private/Security.ps1 index d8521cfb7..559f672d0 100644 --- a/src/Private/Security.ps1 +++ b/src/Private/Security.ps1 @@ -494,25 +494,6 @@ function Add-PodeEndpointLimit { throw "Seconds value cannot be 0 or less for $($IP)" } - # we need to check endpoints on requests - switch ($endpoint.Type.ToLowerInvariant()) { - 'http' { - $PodeContext.Server.FindEndpoints.Route = $true - } - - 'ws' { - $PodeContext.Server.FindEndpoints.Route = $true - } - - 'smtp' { - $PodeContext.Server.FindEndpoints.Smtp = $true - } - - 'tcp' { - $PodeContext.Server.FindEndpoints.Tcp = $true - } - } - # get current rules $rules = $PodeContext.Server.Limits.Rules[$type] diff --git a/src/Private/Server.ps1 b/src/Private/Server.ps1 index 709ad2051..261c8ddb5 100644 --- a/src/Private/Server.ps1 +++ b/src/Private/Server.ps1 @@ -144,7 +144,19 @@ function Start-PodeInternalServer { if ($endpoints.Length -gt 0) { Write-PodeHost "Listening on the following $($endpoints.Length) endpoint(s) [$($PodeContext.Threads.General) thread(s)]:" -ForegroundColor Yellow $endpoints | ForEach-Object { - Write-PodeHost "`t- $($_.Url)" -ForegroundColor Yellow + $flags = @() + if ($_.DualMode) { + $flags += 'DualMode' + } + + if ($flags.Length -eq 0) { + $flags = [string]::Empty + } + else { + $flags = "[$($flags -join ',')]" + } + + Write-PodeHost "`t- $($_.Url) $($flags)" -ForegroundColor Yellow } } } @@ -214,11 +226,6 @@ function Restart-PodeInternalServer { # clear endpoints $PodeContext.Server.Endpoints.Clear() $PodeContext.Server.EndpointsMap.Clear() - $PodeContext.Server.FindEndpoints = @{ - Route = $false - Smtp = $false - Tcp = $false - } # clear openapi $PodeContext.Server.OpenAPI = Get-PodeOABaseObject diff --git a/src/Private/SmtpServer.ps1 b/src/Private/SmtpServer.ps1 index 9ffe337a0..603e08dff 100644 --- a/src/Private/SmtpServer.ps1 +++ b/src/Private/SmtpServer.ps1 @@ -12,13 +12,20 @@ function Start-PodeSmtpServer { @(Get-PodeEndpoints -Type Smtp) | ForEach-Object { # get the ip address $_ip = [string]($_.Address) - $_ip = (Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1) - $_ip = (Get-PodeIPAddress $_ip) + $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 + $_ip = Get-PodeIPAddress $_ip -DualMode:($_.DualMode) + + # dual mode? + $addrs = $_ip + if ($_.DualMode) { + $addrs = Resolve-PodeIPDualMode -IP $_ip + } # the endpoint $_endpoint = @{ + Name = $_.Name Key = "$($_ip):$($_.Port)" - Address = $_ip + Address = $addrs Hostname = $_.HostName IsIPAddress = $_.IsIPAddress Port = $_.Port @@ -31,6 +38,7 @@ function Start-PodeSmtpServer { Pool = $_.Runspace.PoolName Acknowledge = $_.Tcp.Acknowledge SslProtocols = $_.Ssl.Protocols + DualMode = $_.DualMode } # add endpoint to list @@ -47,7 +55,7 @@ function Start-PodeSmtpServer { try { # register endpoints on the listener $endpoints | ForEach-Object { - $socket = [PodeSocket]::new($_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Smtp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode) + $socket = [PodeSocket]::new($_.Name, $_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Smtp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode, $_.DualMode) $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout $socket.AcknowledgeMessage = $_.Acknowledge @@ -109,15 +117,12 @@ function Start-PodeSmtpServer { Endpoint = @{ Protocol = $Request.Scheme Address = $Request.Address - Name = $null + Name = $context.EndpointName } Timestamp = [datetime]::UtcNow Metadata = @{} } - # endpoint name - $SmtpEvent.Endpoint.Name = (Find-PodeEndpointName -Protocol $SmtpEvent.Endpoint.Protocol -Address $SmtpEvent.Endpoint.Address -LocalAddress $SmtpEvent.Request.LocalEndPoint -Enabled:($PodeContext.Server.FindEndpoints.Smtp)) - # stop now if the request has an error if ($Request.IsAborted) { throw $Request.Error @@ -199,8 +204,9 @@ function Start-PodeSmtpServer { # state where we're running return @(foreach ($endpoint in $endpoints) { @{ - Url = $endpoint.Url - Pool = $endpoint.Pool + Url = $endpoint.Url + Pool = $endpoint.Pool + DualMode = $endpoint.DualMode } }) } \ No newline at end of file diff --git a/src/Private/TcpServer.ps1 b/src/Private/TcpServer.ps1 index 5eb99763b..60156d15b 100644 --- a/src/Private/TcpServer.ps1 +++ b/src/Private/TcpServer.ps1 @@ -7,13 +7,20 @@ function Start-PodeTcpServer { @(Get-PodeEndpoints -Type Tcp) | ForEach-Object { # get the ip address $_ip = [string]($_.Address) - $_ip = (Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1) - $_ip = (Get-PodeIPAddress $_ip) + $_ip = Get-PodeIPAddressesForHostname -Hostname $_ip -Type All | Select-Object -First 1 + $_ip = Get-PodeIPAddress $_ip -DualMode:($_.DualMode) + + # dual mode? + $addrs = $_ip + if ($_.DualMode) { + $addrs = Resolve-PodeIPDualMode -IP $_ip + } # the endpoint $_endpoint = @{ + Name = $_.Name Key = "$($_ip):$($_.Port)" - Address = $_ip + Address = $addrs Hostname = $_.HostName IsIPAddress = $_.IsIPAddress Port = $_.Port @@ -27,6 +34,7 @@ function Start-PodeTcpServer { Acknowledge = $_.Tcp.Acknowledge CRLFMessageEnd = $_.Tcp.CRLFMessageEnd SslProtocols = $_.Ssl.Protocols + DualMode = $_.DualMode } # add endpoint to list @@ -43,7 +51,7 @@ function Start-PodeTcpServer { try { # register endpoints on the listener $endpoints | ForEach-Object { - $socket = [PodeSocket]::new($_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Tcp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode) + $socket = [PodeSocket]::new($_.Name, $_.Address, $_.Port, $_.SslProtocols, [PodeProtocolType]::Tcp, $_.Certificate, $_.AllowClientCertificate, $_.TlsMode, $_.DualMode) $socket.ReceiveTimeout = $PodeContext.Server.Sockets.ReceiveTimeout $socket.AcknowledgeMessage = $_.Acknowledge $socket.CRLFMessageEnd = $_.CRLFMessageEnd @@ -94,16 +102,13 @@ function Start-PodeTcpServer { Endpoint = @{ Protocol = $Request.Scheme Address = $Request.Address - Name = $null + Name = $context.EndpointName } Parameters = $null Timestamp = [datetime]::UtcNow Metadata = @{} } - # endpoint name - $TcpEvent.Endpoint.Name = (Find-PodeEndpointName -Protocol $TcpEvent.Endpoint.Protocol -Address $TcpEvent.Endpoint.Address -LocalAddress $TcpEvent.Request.LocalEndPoint -Enabled:($PodeContext.Server.FindEndpoints.Tcp)) - # stop now if the request has an error if ($Request.IsAborted) { throw $Request.Error @@ -218,8 +223,9 @@ function Start-PodeTcpServer { # state where we're running return @(foreach ($endpoint in $endpoints) { @{ - Url = $endpoint.Url - Pool = $endpoint.Pool + Url = $endpoint.Url + Pool = $endpoint.Pool + DualMode = $endpoint.DualMode } }) } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index afe1aefc8..2c65f7e9b 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -751,6 +751,10 @@ If supplied, the endpoint created will be returned. .PARAMETER LookupHostname If supplied, a supplied Hostname will have its IP Address looked up from host file or DNS. +.PARAMETER DualMode +If supplied, this endpoint will listen on both the IPv4 and IPv6 versions of the supplied -Address. +For IPv6, this will only work if the IPv6 address can convert to a valid IPv4 address. + .PARAMETER Default If supplied, this endpoint will be the default one used for internally generating URLs. @@ -872,6 +876,9 @@ function Add-PodeEndpoint { [switch] $LookupHostname, + [switch] + $DualMode, + [switch] $Default ) @@ -954,6 +961,7 @@ function Add-PodeEndpoint { $obj = @{ Name = $Name Description = $Description + DualMode = $DualMode Address = $null RawAddress = $null Port = $null @@ -989,14 +997,16 @@ function Add-PodeEndpoint { } # set the ip for the context (force to localhost for IIS) - $obj.Address = (Get-PodeIPAddress $_endpoint.Host) + $obj.Address = Get-PodeIPAddress $_endpoint.Host -DualMode:$DualMode $obj.IsIPAddress = [string]::IsNullOrWhiteSpace($obj.HostName) if ($obj.IsIPAddress) { - $obj.FriendlyName = 'localhost' if (!(Test-PodeIPAddressLocalOrAny -IP $obj.Address)) { $obj.FriendlyName = "$($obj.Address)" } + else { + $obj.FriendlyName = 'localhost' + } } # set the port for the context, if 0 use a default port for protocol diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 6f91496da..20c324006 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -258,10 +258,6 @@ function Add-PodeRoute { $Path = Resolve-PodePlaceholders -Path $Path # get endpoints from name - if (!$PodeContext.Server.FindEndpoints.Route) { - $PodeContext.Server.FindEndpoints.Route = !(Test-PodeIsEmpty $EndpointName) - } - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName # get default route IfExists state @@ -667,10 +663,6 @@ function Add-PodeStaticRoute { $Path = Resolve-PodePlaceholders -Path $Path # get endpoints from name - if (!$PodeContext.Server.FindEndpoints.Route) { - $PodeContext.Server.FindEndpoints.Route = !(Test-PodeIsEmpty $EndpointName) - } - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName # get default route IfExists state @@ -901,10 +893,6 @@ function Add-PodeSignalRoute { $Path = Update-PodeRouteSlashes -Path $Path # get endpoints from name - if (!$PodeContext.Server.FindEndpoints.Route) { - $PodeContext.Server.FindEndpoints.Route = !(Test-PodeIsEmpty $EndpointName) - } - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName # get default route IfExists state diff --git a/src/Public/Verbs.ps1 b/src/Public/Verbs.ps1 index 8ef239b81..63cd58b9c 100644 --- a/src/Public/Verbs.ps1 +++ b/src/Public/Verbs.ps1 @@ -73,10 +73,6 @@ function Add-PodeVerb { $Verb = Resolve-PodePlaceholders -Path $Verb # get endpoints from name - if (!$PodeContext.Server.FindEndpoints.Tcp) { - $PodeContext.Server.FindEndpoints.Tcp = !(Test-PodeIsEmpty $EndpointName) - } - $endpoints = Find-PodeEndpoints -EndpointName $EndpointName # ensure the verb doesn't already exist for each endpoint diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index b39695c5b..8842929bc 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -90,7 +90,7 @@ Describe 'Test-PodeIsEmpty' { } It 'Return true for a whitespace string' { - Test-PodeIsEmpty -Value " " | Should Be $true + Test-PodeIsEmpty -Value ' ' | Should Be $true } It 'Return true for an empty scriptblock' { @@ -100,7 +100,7 @@ Describe 'Test-PodeIsEmpty' { Context 'Valid value is passed' { It 'Return false for a string' { - Test-PodeIsEmpty -Value "test" | Should Be $false + Test-PodeIsEmpty -Value 'test' | Should Be $false } It 'Return false for a number' { @@ -112,7 +112,7 @@ Describe 'Test-PodeIsEmpty' { } It 'Return false for a hashtable' { - Test-PodeIsEmpty -Value @{'key'='value';} | Should Be $false + Test-PodeIsEmpty -Value @{'key' = 'value'; } | Should Be $false } It 'Return false for a scriptblock' { @@ -183,11 +183,11 @@ Describe 'Get-PodeHostIPRegex' { } It 'Returns valid IP regex' { - Get-PodeHostIPRegex -Type IP | Should Be '(?(\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all))' + Get-PodeHostIPRegex -Type IP | Should Be '(?(\[?([a-f0-9]*\:){1,}[a-f0-9]*((\d+\.){3}\d+)?\]?|((\d+\.){3}\d+)|\*|all))' } It 'Returns valid IP and Hostname regex' { - Get-PodeHostIPRegex -Type Both | Should Be '(?(\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all|([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+))' + Get-PodeHostIPRegex -Type Both | Should Be '(?(\[?([a-f0-9]*\:){1,}[a-f0-9]*((\d+\.){3}\d+)?\]?|((\d+\.){3}\d+)|\*|all|([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+))' } } @@ -616,7 +616,7 @@ Describe 'ConvertFrom-PodeRequestContent' { $value = 'test' $result = ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'text/xml' @@ -630,7 +630,7 @@ Describe 'ConvertFrom-PodeRequestContent' { $value = '{ "value": "test" }' $result = ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'application/json' @@ -643,7 +643,7 @@ Describe 'ConvertFrom-PodeRequestContent' { $value = "value`ntest" $result = ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'text/csv' @@ -653,10 +653,10 @@ Describe 'ConvertFrom-PodeRequestContent' { It 'Returns original data' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http'; 'BodyParsers' = @{} } } - $value = "test" + $value = 'test' (ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'text/custom').Data | Should Be 'test' } @@ -665,8 +665,8 @@ Describe 'ConvertFrom-PodeRequestContent' { $PodeContext = @{ 'Server' = @{ 'ServerlessType' = 'AzureFunctions'; 'BodyParsers' = @{}; 'IsServerless' = $true } } $result = ConvertFrom-PodeRequestContent -Request @{ - 'ContentEncoding' = [System.Text.Encoding]::UTF8; - 'RawBody' = '{ "value": "test" }'; + 'ContentEncoding' = [System.Text.Encoding]::UTF8 + 'RawBody' = '{ "value": "test" }' } -ContentType 'application/json' $result.Data | Should Not Be $null @@ -677,8 +677,8 @@ Describe 'ConvertFrom-PodeRequestContent' { $PodeContext = @{ 'Server' = @{ 'ServerlessType' = 'AwsLambda'; 'BodyParsers' = @{}; 'IsServerless' = $true } } $result = ConvertFrom-PodeRequestContent -Request @{ - 'ContentEncoding' = [System.Text.Encoding]::UTF8; - 'body' = '{ "value": "test" }'; + 'ContentEncoding' = [System.Text.Encoding]::UTF8 + 'body' = '{ "value": "test" }' } -ContentType 'application/json' $result.Data | Should Not Be $null @@ -704,13 +704,13 @@ Describe 'Test-PodePathIsFile' { } Describe 'Test-PodePathIsWildcard' { - It 'Returns true for a wildcard' { - Test-PodePathIsWildcard -Path './some/path/*' | Should Be $true - } + It 'Returns true for a wildcard' { + Test-PodePathIsWildcard -Path './some/path/*' | Should Be $true + } - It 'Returns false for no wildcard' { - Test-PodePathIsWildcard -Path './some/path/folder' | Should Be $false - } + It 'Returns false for no wildcard' { + Test-PodePathIsWildcard -Path './some/path/folder' | Should Be $false + } } Describe 'Test-PodePathIsDirectory' { @@ -873,9 +873,9 @@ Describe 'Get-PodeUrl' { $WebEvent = @{ Endpoint = @{ Protocol = 'http' - Address = 'foo.com/'; + Address = 'foo.com/' } - Path = 'about' + Path = 'about' } Get-PodeUrl | Should Be 'http://foo.com/about' @@ -1046,7 +1046,7 @@ Describe 'Get-PodeRelativePath' { } } - Get-PodeRelativePath -Path ".\src" -Resolve -JoinRoot | Should Be (Join-Path $pwd.Path "src") + Get-PodeRelativePath -Path '.\src' -Resolve -JoinRoot | Should Be (Join-Path $pwd.Path 'src') } It 'Returns path for a relative path joined to default root' { @@ -1060,7 +1060,7 @@ Describe 'Get-PodeRelativePath' { } } - Get-PodeRelativePath -Path './src' -JoinRoot -Resolve | Should Be (Join-Path $pwd.Path "src") + Get-PodeRelativePath -Path './src' -JoinRoot -Resolve | Should Be (Join-Path $pwd.Path 'src') } It 'Returns path for a relative path joined to passed root' { @@ -1155,27 +1155,28 @@ Describe 'Close-PodeServerInternal' { Describe 'Get-PodeEndpointUrl' { It 'Returns default endpoint url' { $PodeContext = @{ Server = @{ - Endpoints = @{ - Example1 = @{ - Port = 6000 - Address = '127.0.0.1' - FriendlyName = 'thing.com' - Hostname = 'thing.com' - Protocol = 'https' + Endpoints = @{ + Example1 = @{ + Port = 6000 + Address = '127.0.0.1' + FriendlyName = 'thing.com' + Hostname = 'thing.com' + Protocol = 'https' + } } } - } } + } Get-PodeEndpointUrl | Should Be 'https://thing.com:6000' } It 'Returns a passed endpoint url' { $endpoint = @{ - Port = 7000 - Address = '127.0.0.1' + Port = 7000 + Address = '127.0.0.1' FriendlyName = 'stuff.com' - Hostname = 'stuff.com' - Protocol = 'http' + Hostname = 'stuff.com' + Protocol = 'http' } Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'http://stuff.com:7000' @@ -1183,11 +1184,11 @@ Describe 'Get-PodeEndpointUrl' { It 'Returns a passed endpoint url, with default port for http' { $endpoint = @{ - Port = 8080 - Address = '127.0.0.1' + Port = 8080 + Address = '127.0.0.1' FriendlyName = 'stuff.com' - Hostname = 'stuff.com' - Protocol = 'http' + Hostname = 'stuff.com' + Protocol = 'http' } Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'http://stuff.com:8080' @@ -1195,11 +1196,11 @@ Describe 'Get-PodeEndpointUrl' { It 'Returns a passed endpoint url, with default port for https' { $endpoint = @{ - Port = 8443 - Address = '127.0.0.1' + Port = 8443 + Address = '127.0.0.1' FriendlyName = 'stuff.com' - Hostname = 'stuff.com' - Protocol = 'https' + Hostname = 'stuff.com' + Protocol = 'https' } Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'https://stuff.com:8443' @@ -1216,11 +1217,11 @@ Describe 'Get-PodeEndpointUrl' { Describe 'Get-PodeCount' { Context 'Null' { - It 'Null value'{ + It 'Null value' { Get-PodeCount $null | Should Be 0 } } - Context 'String'{ + Context 'String' { It 'Empty' { Get-PodeCount '' | Should Be 0 } @@ -1231,25 +1232,25 @@ Describe 'Get-PodeCount' { } } - Context 'Numbers'{ - It 'Number'{ + Context 'Numbers' { + It 'Number' { Get-PodeCount 2 | Should Be 1 } } Context 'Array' { - It 'Empty'{ + It 'Empty' { Get-PodeCount @() | Should Be 0 } - It 'One'{ + It 'One' { Get-PodeCount @(4) | Should Be 1 Get-PodeCount @('data') | Should Be 1 Get-PodeCount @(@(3)) | Should Be 1 Get-PodeCount @(@{}) | Should Be 1 } - It 'Two'{ + It 'Two' { Get-PodeCount @(4, 7) | Should Be 2 Get-PodeCount @('data', 9) | Should Be 2 Get-PodeCount @(@(3), @()) | Should Be 2 @@ -1258,29 +1259,29 @@ Describe 'Get-PodeCount' { } Context 'Hashtable' { - It 'Empty'{ + It 'Empty' { Get-PodeCount @{} | Should Be 0 } - It 'One'{ - Get-PodeCount @{'testElement1'=4} | Should Be 1 - Get-PodeCount @{'testElement1'='test'} | Should Be 1 - Get-PodeCount @{'testElement1'=@()} | Should Be 1 - Get-PodeCount @{'testElement1'=@{"insideElement"="won't count"}} | Should Be 1 + It 'One' { + Get-PodeCount @{'testElement1' = 4 } | Should Be 1 + Get-PodeCount @{'testElement1' = 'test' } | Should Be 1 + Get-PodeCount @{'testElement1' = @() } | Should Be 1 + Get-PodeCount @{'testElement1' = @{'insideElement' = "won't count" } } | Should Be 1 } - It 'Two'{ - Get-PodeCount @{'testElement1'=4; 'testElement2'=10} | Should Be 2 - Get-PodeCount @{'testElement1'='test'; 'testElement2'=10} | Should Be 2 - Get-PodeCount @{'testElement1'=@(); 'testElement2'=@(9)} | Should Be 2 - Get-PodeCount @{'testElement1'=@{"insideElement"="won't count"}; 'testElement2'=@('testing')} | Should Be 2 + It 'Two' { + Get-PodeCount @{'testElement1' = 4; 'testElement2' = 10 } | Should Be 2 + Get-PodeCount @{'testElement1' = 'test'; 'testElement2' = 10 } | Should Be 2 + Get-PodeCount @{'testElement1' = @(); 'testElement2' = @(9) } | Should Be 2 + Get-PodeCount @{'testElement1' = @{'insideElement' = "won't count" }; 'testElement2' = @('testing') } | Should Be 2 } } } Describe 'Convert-PodePathSeparators' { Context 'Null' { - It 'Null'{ + It 'Null' { Convert-PodePathSeparators -Path $null | Should Be $null } } @@ -1307,8 +1308,8 @@ Describe 'Convert-PodePathSeparators' { } } - Context 'Array'{ - It 'Null'{ + Context 'Array' { + It 'Null' { Convert-PodePathSeparators -Path @($null) | Should Be $null Convert-PodePathSeparators -Path @($null, $null) | Should Be $null } @@ -1411,51 +1412,51 @@ Describe 'Convert-PodeQueryStringToHashTable' { } It 'Emty for uri but no query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/users" + $result = Convert-PodeQueryStringToHashTable -Uri '/api/users' $result.Count | Should Be 0 } It 'Hashtable for root query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/?Name=Bob" + $result = Convert-PodeQueryStringToHashTable -Uri '/?Name=Bob' $result.Count | Should Be 1 $result['Name'] | Should Be 'Bob' } It 'Hashtable for root query, no slash' { - $result = Convert-PodeQueryStringToHashTable -Uri "?Name=Bob" + $result = Convert-PodeQueryStringToHashTable -Uri '?Name=Bob' $result.Count | Should Be 1 $result['Name'] | Should Be 'Bob' } It 'Hashtable for root multi-query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/?Name=Bob&Age=42" + $result = Convert-PodeQueryStringToHashTable -Uri '/?Name=Bob&Age=42' $result.Count | Should Be 2 $result['Name'] | Should Be 'Bob' $result['Age'] | Should Be 42 } It 'Hashtable for root multi-query, no slash' { - $result = Convert-PodeQueryStringToHashTable -Uri "?Name=Bob&Age=42" + $result = Convert-PodeQueryStringToHashTable -Uri '?Name=Bob&Age=42' $result.Count | Should Be 2 $result['Name'] | Should Be 'Bob' $result['Age'] | Should Be 42 } It 'Hashtable for non-root query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/user?Name=Bob" + $result = Convert-PodeQueryStringToHashTable -Uri '/api/user?Name=Bob' $result.Count | Should Be 1 $result['Name'] | Should Be 'Bob' } It 'Hashtable for non-root multi-query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/user?Name=Bob&Age=42" + $result = Convert-PodeQueryStringToHashTable -Uri '/api/user?Name=Bob&Age=42' $result.Count | Should Be 2 $result['Name'] | Should Be 'Bob' $result['Age'] | Should Be 42 } It 'Hashtable for non-root multi-query, end slash' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/user/?Name=Bob&Age=42" + $result = Convert-PodeQueryStringToHashTable -Uri '/api/user/?Name=Bob&Age=42' $result.Count | Should Be 2 $result['Name'] | Should Be 'Bob' $result['Age'] | Should Be 42 @@ -1497,7 +1498,7 @@ Describe 'ConvertFrom-PodeHeaderQValue' { Describe 'Get-PodeAcceptEncoding' { $PodeContext = @{ Server = @{ - Web = @{ Compression = @{ Enabled = $true } } + Web = @{ Compression = @{ Enabled = $true } } Compression = @{ Encodings = @('gzip', 'deflate', 'x-gzip') } } } diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index 5ae80a3d2..66dff5eb8 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -31,7 +31,7 @@ Describe 'Find-PodeRoute' { } It 'Returns logic for method and exact route' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Logic'= { Write-Host 'Test' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Logic' = { Write-Host 'Test' }; }); }; }; } $result = (Find-PodeRoute -Method GET -Path '/') $result | Should BeOfType System.Collections.Hashtable @@ -40,9 +40,12 @@ Describe 'Find-PodeRoute' { It 'Returns logic for method and exact route and endpoint' { $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @( - @{ 'Logic'= { Write-Host 'Test' }; }; - @{ 'Logic'= { Write-Host 'Test' }; 'Endpoint' = @{ Name = 'example'; 'Address' = 'pode.foo.com' } }; - ); }; }; } + @{ 'Logic' = { Write-Host 'Test' }; } + @{ 'Logic' = { Write-Host 'Test' }; 'Endpoint' = @{ Name = 'example'; 'Address' = 'pode.foo.com' } } + ) + } + } + } $result = (Find-PodeRoute -Method GET -Path '/' -EndpointName 'example') @@ -52,7 +55,7 @@ Describe 'Find-PodeRoute' { } It 'Returns logic and middleware for method and exact route' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Logic'= { Write-Host 'Test' }; 'Middleware' = { Write-Host 'Middle' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Logic' = { Write-Host 'Test' }; 'Middleware' = { Write-Host 'Middle' }; }); }; }; } $result = (Find-PodeRoute -Method GET -Path '/') $result | Should BeOfType System.Collections.Hashtable @@ -61,7 +64,7 @@ Describe 'Find-PodeRoute' { } It 'Returns logic for method and exact route under star' { - $PodeContext.Server = @{ 'Routes' = @{ '*' = @{ '/' = @(@{ 'Logic'= { Write-Host 'Test' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ '*' = @{ '/' = @(@{ 'Logic' = { Write-Host 'Test' }; }); }; }; } $result = (Find-PodeRoute -Method * -Path '/') $result | Should BeOfType System.Collections.Hashtable @@ -69,7 +72,7 @@ Describe 'Find-PodeRoute' { } It 'Returns logic and parameters for parameterised route' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/(?[^\/]+?)' = @(@{ 'Logic'= { Write-Host 'Test' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/(?[^\/]+?)' = @(@{ 'Logic' = { Write-Host 'Test' }; }); }; }; } $result = (Find-PodeRoute -Method GET -Path '/123') $result | Should BeOfType System.Collections.Hashtable @@ -83,7 +86,7 @@ Describe 'Add-PodeStaticRoute' { Mock Test-PodePath { return $true } Mock New-PodePSDrive { return './assets' } - $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd; FindEndpoints = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' $route = $PodeContext.Server.Routes['static'] @@ -94,14 +97,14 @@ Describe 'Add-PodeStaticRoute' { It 'Throws error when adding static route for non-existing folder' { Mock Test-PodePath { return $false } - $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd; FindEndpoints = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd } { Add-PodeStaticRoute -Path '/assets' -Source './assets' } | Should Throw 'does not exist' } } Describe 'Remove-PodeRoute' { It 'Adds route with simple url, and then removes it' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -118,7 +121,7 @@ Describe 'Remove-PodeRoute' { } It 'Adds two routes with simple url, and then removes one' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ GET = @{}; }; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user @@ -144,7 +147,7 @@ Describe 'Remove-PodeStaticRoute' { Mock Test-PodePath { return $true } Mock New-PodePSDrive { return './assets' } - $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd; FindEndpoints = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' $routes = $PodeContext.Server.Routes['static'] @@ -162,7 +165,7 @@ Describe 'Remove-PodeStaticRoute' { Describe 'Clear-PodeRoutes' { It 'Adds routes for methods, and clears everything' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello1' } Add-PodeRoute -Method POST -Path '/messages' -ScriptBlock { Write-Host 'hello2' } @@ -182,7 +185,7 @@ Describe 'Clear-PodeRoutes' { } It 'Adds routes for methods, and clears one method' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello1' } Add-PodeRoute -Method POST -Path '/messages' -ScriptBlock { Write-Host 'hello2' } @@ -207,7 +210,7 @@ Describe 'Clear-PodeStaticRoutes' { Mock Test-PodePath { return $true } Mock New-PodePSDrive { return './assets' } - $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd; FindEndpoints = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' Add-PodeStaticRoute -Path '/images' -Source './images' @@ -244,40 +247,43 @@ Describe 'Add-PodeRoute' { It 'Throws error when file path is a directory' { Mock Get-PodeRelativePath { return $Path } Mock Test-PodePath { return $true } - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{} }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{} } } { Add-PodeRoute -Method GET -Path '/' -FilePath './path' } | Should Throw 'cannot be a wildcard or a directory' } It 'Throws error when file path is a wildcard' { Mock Get-PodeRelativePath { return $Path } Mock Test-PodePath { return $true } - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{} }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{} } } { Add-PodeRoute -Method GET -Path '/' -FilePath './path/*' } | Should Throw 'cannot be a wildcard or a directory' } It 'Throws error because no scriptblock supplied' { - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should Throw "No logic passed" + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should Throw 'No logic passed' } It 'Throws error because only querystring has been given' { - { Add-PodeRoute -Method GET -Path "?k=v" -ScriptBlock { write-host 'hi' } } | Should Throw "No path supplied" + { Add-PodeRoute -Method GET -Path '?k=v' -ScriptBlock { write-host 'hi' } } | Should Throw 'No path supplied' } It 'Throws error because route already exists' { $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @( - @{ 'Endpoint' = @{'Protocol' = ''; 'Address' = ''} } - ); }; }; 'FindEndpoints' = @{} } + @{ 'Endpoint' = @{'Protocol' = ''; 'Address' = '' } } + ) + } + } + } { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } } | Should Throw 'already defined' } It 'Throws error on GET route for endpoint name not existing' { - $PodeContext.Server = @{ 'Endpoints' = @{}; 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Endpoints' = @{}; 'Routes' = @{ 'GET' = @{}; } } { Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName 'test' } | Should Throw 'does not exist' } It 'Adds route with simple url' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] @@ -295,7 +301,7 @@ Describe 'Add-PodeRoute' { Mock Test-PodePath { return $true } Mock Use-PodeScript { return { Write-Host 'bye' } } - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -FilePath './path/route.ps1' $routes = $PodeContext.Server.Routes['get'] @@ -311,7 +317,7 @@ Describe 'Add-PodeRoute' { Mock Test-PodePath { return $false } It 'Adds route with simple url with content type' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -ContentType 'application/json' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] @@ -326,12 +332,12 @@ Describe 'Add-PodeRoute' { It 'Adds route with simple url with default content type' { $PodeContext.Server = @{ - 'Routes' = @{ 'GET' = @{}; }; - 'Web' = @{ 'ContentType' = @{ - 'Default' = 'text/xml'; - 'Routes' = @{}; - } }; - 'FindEndpoints' = @{} + 'Routes' = @{ 'GET' = @{}; } + 'Web' = @{ 'ContentType' = @{ + 'Default' = 'text/xml' + 'Routes' = @{} + } + } } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -348,12 +354,12 @@ Describe 'Add-PodeRoute' { It 'Adds route with simple url with route pattern content type' { $PodeContext.Server = @{ - 'Routes' = @{ 'GET' = @{}; }; - 'Web' = @{ 'ContentType' = @{ - 'Default' = 'text/xml'; - 'Routes' = @{ '/users' = 'text/plain' }; - } }; - 'FindEndpoints' = @{} + 'Routes' = @{ 'GET' = @{}; } + 'Web' = @{ 'ContentType' = @{ + 'Default' = 'text/xml' + 'Routes' = @{ '/users' = 'text/plain' } + } + } } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -369,7 +375,7 @@ Describe 'Add-PodeRoute' { } It 'Adds route with middleware supplied as scriptblock and no logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -Middleware ({ Write-Host 'middle' }) -ScriptBlock {} $route = $PodeContext.Server.Routes['get'] @@ -383,22 +389,22 @@ Describe 'Add-PodeRoute' { } It 'Adds route with middleware supplied as hashtable with null logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = $null }) -ScriptBlock {} } | Should Throw 'no logic defined' } It 'Adds route with middleware supplied as hashtable with invalid type logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = 74 }) -ScriptBlock {} } | Should Throw 'invalid logic type' } It 'Adds route with invalid middleware type' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } { Add-PodeRoute -Method GET -Path '/users' -Middleware 74 -ScriptBlock {} } | Should Throw 'invalid type' } It 'Adds route with middleware supplied as hashtable and empty logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{}} + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; } Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = { Write-Host 'middle' }; 'Arguments' = 'test' }) -ScriptBlock {} $routes = $PodeContext.Server.Routes['get'] @@ -418,7 +424,7 @@ Describe 'Add-PodeRoute' { } It 'Adds route with middleware supplied as hashtable and no logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = { Write-Host 'middle' }; 'Arguments' = 'test' }) -ScriptBlock {} $routes = $PodeContext.Server.Routes['get'] @@ -438,7 +444,7 @@ Describe 'Add-PodeRoute' { } It 'Adds route with middleware and logic supplied' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -Middleware { Write-Host 'middle' } -ScriptBlock { Write-Host 'logic' } $routes = $PodeContext.Server.Routes['get'] @@ -457,12 +463,12 @@ Describe 'Add-PodeRoute' { } It 'Adds route with array of middleware and no logic supplied' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -Middleware @( { Write-Host 'middle1' }, { Write-Host 'middle2' } - ) -ScriptBlock {} + ) -ScriptBlock {} $routes = $PodeContext.Server.Routes['get'] $routes | Should Not be $null @@ -479,11 +485,11 @@ Describe 'Add-PodeRoute' { } It 'Adds route with array of middleware and logic supplied' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users' -Middleware @( { Write-Host 'middle1' }, { Write-Host 'middle2' } - ) -ScriptBlock { Write-Host 'logic' } + ) -ScriptBlock { Write-Host 'logic' } $route = $PodeContext.Server.Routes['get'] $route | Should Not be $null @@ -498,7 +504,7 @@ Describe 'Add-PodeRoute' { } It 'Adds route with simple url and querystring' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users?k=v' -ScriptBlock { Write-Host 'hello' } $route = $PodeContext.Server.Routes['get'] @@ -510,7 +516,7 @@ Describe 'Add-PodeRoute' { } It 'Adds route with url parameters' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users/:userId' -ScriptBlock { Write-Host 'hello' } $route = $PodeContext.Server.Routes['get'] @@ -522,7 +528,7 @@ Describe 'Add-PodeRoute' { } It 'Adds route with url parameters and querystring' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; } } Add-PodeRoute -Method GET -Path '/users/:userId?k=v' -ScriptBlock { Write-Host 'hello' } $route = $PodeContext.Server.Routes['get'] @@ -768,16 +774,16 @@ Describe 'Get-PodeRouteByUrl' { $routeNameSet = @{ Endpoint = @{ Protocol = 'HTTP' - Address = '/assets' - Name = 'Example1' + Address = '/assets' + Name = 'Example1' } } $routeNoNameSet = @{ Endpoint = @{ Protocol = '' - Address = '/assets' - Name = 'Example2' + Address = '/assets' + Name = 'Example2' } } @@ -813,7 +819,7 @@ Describe 'Get-PodeRoute' { Mock Test-PodeIsAdminUser { return $true } It 'Returns both routes whe nothing supplied' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; } } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -823,7 +829,7 @@ Describe 'Get-PodeRoute' { } It 'Returns both routes for GET method' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; } } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -833,7 +839,7 @@ Describe 'Get-PodeRoute' { } It 'Returns one route for POST method' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; } } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -843,7 +849,7 @@ Describe 'Get-PodeRoute' { } It 'Returns both routes for users path' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; } } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -853,7 +859,7 @@ Describe 'Get-PodeRoute' { } It 'Returns one route for users path and GET method' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; } } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } @@ -863,7 +869,7 @@ Describe 'Get-PodeRoute' { } It 'Returns one route for users path and endpoint name user' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -878,7 +884,7 @@ Describe 'Get-PodeRoute' { } It 'Returns both routes for users path and endpoint names' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -891,7 +897,7 @@ Describe 'Get-PodeRoute' { } It 'Returns both routes for user endpoint name' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -909,7 +915,7 @@ Describe 'Get-PodeStaticRoute' { Mock New-PodePSDrive { return './assets' } It 'Returns all static routes' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; FindEndpoints = @{}; Root = $pwd } + $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' Add-PodeStaticRoute -Path '/images' -Source './images' @@ -918,7 +924,7 @@ Describe 'Get-PodeStaticRoute' { } It 'Returns one static route' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; FindEndpoints = @{}; Root = $pwd } + $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' Add-PodeStaticRoute -Path '/images' -Source './images' @@ -927,7 +933,7 @@ Describe 'Get-PodeStaticRoute' { } It 'Returns one static route for endpoint name user' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -942,7 +948,7 @@ Describe 'Get-PodeStaticRoute' { } It 'Returns both routes for users path and endpoint names' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -955,7 +961,7 @@ Describe 'Get-PodeStaticRoute' { } It 'Returns both routes for user endpoint' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } + $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; Root = $pwd; Endpoints = @{}; EndpointsMap = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -984,8 +990,11 @@ Describe 'Find-PodeRouteTransferEncoding' { It 'Returns a path match' { $PodeContext.Server = @{ Web = @{ TransferEncoding = @{ Routes = @{ - '/users' = 'text/json' - } } } } + '/users' = 'text/json' + } + } + } + } Find-PodeRouteTransferEncoding -Path '/users' | Should Be 'text/json' } @@ -1007,8 +1016,11 @@ Describe 'Find-PodeRouteContentType' { It 'Returns a path match' { $PodeContext.Server = @{ Web = @{ ContentType = @{ Routes = @{ - '/users' = 'text/json' - } } } } + '/users' = 'text/json' + } + } + } + } Find-PodeRouteContentType -Path '/users' | Should Be 'text/json' } From 1cfdadc351dffc69dc7283fe1f34a1ed76c37008 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 09:36:26 -0700 Subject: [PATCH 30/84] Fix #1257 Server stops with errors "Collection was modified after..." #1257 --- src/Public/Routes.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 6f91496da..9a439cdb8 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -2166,10 +2166,10 @@ function Add-PodePage { # invoke the function (optional splat data) if (Test-PodeIsEmpty $data) { - $result = (. $script) + $result = & $script } else { - $result = (. $script @data) + $result = & $script @data } # if we have a result, convert it to html From 36d7e0e6c66fe9674ee101048e6ba1a1557bb273 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 10:16:57 -0700 Subject: [PATCH 31/84] Implement #1259 Note Pester 5.5 migration #1260 is required to make the new build works --- README.md | 2 + docs/Getting-Started/build.md | 192 ++++++++++++++ .../{pode.nuspec => pode_template.nuspec} | 0 ...all.ps1 => ChocolateyInstall_template.ps1} | 0 pode.build.ps1 | 251 ++++++++++++++---- src/Listener/Pode.csproj | 2 +- 6 files changed, 397 insertions(+), 50 deletions(-) create mode 100644 docs/Getting-Started/build.md rename packers/choco/{pode.nuspec => pode_template.nuspec} (100%) rename packers/choco/tools/{ChocolateyInstall.ps1 => ChocolateyInstall_template.ps1} (100%) diff --git a/README.md b/README.md index f22f752a6..ad68e8f82 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ To just build Pode, before running any examples, run the following: Invoke-Build Build ``` +More information on how to build Pode can be [found here](./docs/Getting-Started/build.md) + To work on issues you can fork Pode, and then open a Pull Request for approval. Pull Requests should be made against the `develop` branch. Each Pull Request should also have an appropriate issue created. ## 🌎 Roadmap diff --git a/docs/Getting-Started/build.md b/docs/Getting-Started/build.md new file mode 100644 index 000000000..b335cfd9c --- /dev/null +++ b/docs/Getting-Started/build.md @@ -0,0 +1,192 @@ + +# Build Pode locally + +To build and use the code checked out on your machine, follow these steps : + +## Windows + +1. Install InvokeBuild Module + + ### Using Powershell Gallery + + ```powershell + Install-Module InvokeBuild -Scope CurrentUser + ``` + + ### Using Chocolatey + + ```powershell + choco install invoke-build + ``` + +2. Test + + To run the unit tests, run the following command from the root of the repository (this will build Pode and, if needed, auto-install Pester/.NET): + + ```powershell + Invoke-Build Test + ``` + +3. Build + + To just build Pode, before running any examples, run the following: + + ```powershell + Invoke-Build Build + ``` + +4. Packaging + + To create a Pode package. Please note that docker has to be present to create the containers. + + ```powershell + Invoke-Build Pack + ``` + +5. Install locally + + To install Pode from the repository, run the following: + + ```powershell + Invoke-Build Install-Module + ``` + + To uninstall, use : + ```powershell + Invoke-Build Remove-Module + ``` + + +6. CleanUp + + To clean up after a build or a pack, run the following: + + ```powershell + Invoke-Build clean + ``` + +## Linux + +1. Register the Microsoft Repository + + #### Centos + ```shell + sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc + sudo curl -o /etc/yum.repos.d/microsoft.repo https://packages.microsoft.com/config/centos/8/prod.repo + ``` + + #### ReadHat + ```shell + sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc + sudo curl -o /etc/yum.repos.d/microsoft.repo https://packages.microsoft.com/config/rhel/9/prod.repo + ``` + + #### Debian / Ubuntu + ```shell + sudo apt-get update + sudo apt-get install -y wget apt-transport-https software-properties-common + wget https://packages.microsoft.com/config/debian/$(lsb_release -rs)/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + sudo apt-get update; + ``` + + +3. Install InvokeBuild Module + + ```powershell + Install-Module InvokeBuild -Scope CurrentUser + ``` + + +4. Test + + To run the unit tests, run the following command from the root of the repository (this will build Pode and, if needed, auto-install Pester/.NET): + + ```powershell + Invoke-Build Test + ``` + +5. Build + + To just build Pode, before running any examples, run the following: + + ```powershell + Invoke-Build Build + ``` + +6. Packaging + + To create a Pode package. Please note that docker has to be present to create the containers. + + ```powershell + Invoke-Build Pack + ``` + +7. Install locally + + To install Pode from the repository, run the following: + + ```powershell + Invoke-Build Install-Module + ``` + + To uninstall, use : + + ```powershell + Invoke-Build Remove-Module + ``` + + +## MacOS + +An easy way to install the required componentS is to use [brew](https://brew.sh/) + +1. Install dotNet + + ```shell + brew install dotnet + ``` + +2. Install InvokeBuild Module + + ```powershell + Install-Module InvokeBuild -Scope CurrentUser + ``` + +3. Test + + To run the unit tests, run the following command from the root of the repository (this will build Pode and, if needed, auto-install Pester/.NET): + + ```powershell + Invoke-Build Test + ``` + +4. Build + + To just build Pode, before running any examples, run the following: + + ```powershell + Invoke-Build Build + ``` + +5. Packaging + + To create a Pode package. Please note that docker has to be present to create the containers. + + ```powershell + Invoke-Build Pack + ``` + +6. Install locally + + To install Pode from the repository, run the following: + + ```powershell + Invoke-Build Install-Module + ``` + + To uninstall, use : + + ```powershell + Invoke-Build Remove-Module + ``` + diff --git a/packers/choco/pode.nuspec b/packers/choco/pode_template.nuspec similarity index 100% rename from packers/choco/pode.nuspec rename to packers/choco/pode_template.nuspec diff --git a/packers/choco/tools/ChocolateyInstall.ps1 b/packers/choco/tools/ChocolateyInstall_template.ps1 similarity index 100% rename from packers/choco/tools/ChocolateyInstall.ps1 rename to packers/choco/tools/ChocolateyInstall_template.ps1 diff --git a/pode.build.ps1 b/pode.build.ps1 index a0366fd10..82ea8aacf 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -1,6 +1,11 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')] param( [string] - $Version = '' + $Version = '0.0.0', + [string] + [ValidateSet( 'None', 'Normal' , 'Detailed', 'Diagnostic')] + $PesterVerbosity = 'Normal' ) <# @@ -8,12 +13,11 @@ param( #> $Versions = @{ - Pester = '4.8.0' + Pester = '5.5.0' MkDocs = '1.5.3' PSCoveralls = '1.0.0' SevenZip = '18.5.0.20180730' - DotNet = '7.0.1' - Checksum = '0.2.0' + DotNet = '8.0' MkDocsTheme = '9.4.6' PlatyPS = '0.14.2' } @@ -21,7 +25,6 @@ $Versions = @{ <# # Helper Functions #> - function Test-PodeBuildIsWindows { $v = $PSVersionTable return ($v.Platform -ilike '*win*' -or ($null -eq $v.Platform -and $v.PSEdition -ieq 'desktop')) @@ -87,16 +90,45 @@ function Install-PodeBuildModule($name) { Install-Module -Name "$($name)" -Scope CurrentUser -RequiredVersion "$($Versions[$name])" -Force -SkipPublisherCheck } -function Invoke-PodeBuildDotnetBuild($target) { - dotnet build --configuration Release --self-contained --framework $target +function Invoke-PodeBuildDotnetBuild($target,$Version) { + + # Retrieve the highest installed SDK version + $highestSdkVersion = dotnet --list-sdks | Select-Object -Last 1 | ForEach-Object { $_.Split(' ')[0] } + $majorVersion = [int]$highestSdkVersion.Split('.')[0] + + # Determine if the target framework is compatible + $isCompatible = $False + switch ($majorVersion) { + 8 { if ($target -in @('net6.0', 'net7.0', 'netstandard2.0', 'net8.0')) { $isCompatible = $True } } + 7 { if ($target -in @('net6.0', 'net7.0', 'netstandard2.0')) { $isCompatible = $True } } + 6 { if ($target -in @('net6.0', 'netstandard2.0')) { $isCompatible = $True } } + } + + # Skip build if not compatible + if ( $isCompatible) { + Write-Host "SDK for target framework $target is compatible with the installed SDKs" + } + else { + Write-Host "SDK for target framework $target is not compatible with the installed SDKs. Skipping build." + return + } + if ($Version) { + Write-Host "Assembly Version $Version" + $AssemblyVersion = "-p:Version=$Version" + } + else { + $AssemblyVersion = '' + } + dotnet build --configuration Release --self-contained --framework $target $AssemblyVersion if (!$?) { throw "dotnet build failed for $($target)" } - dotnet publish --configuration Release --self-contained --framework $target --output ../Libs/$target + dotnet publish --configuration Release --self-contained --framework $target $AssemblyVersion --output ../Libs/$target if (!$?) { throw "dotnet publish failed for $($target)" } + } @@ -105,22 +137,16 @@ function Invoke-PodeBuildDotnetBuild($target) { #> # Synopsis: Stamps the version onto the Module -task StampVersion { +Task StampVersion { (Get-Content ./pkg/Pode.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./pkg/Pode.psd1 (Get-Content ./pkg/Pode.Internal.psd1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./pkg/Pode.Internal.psd1 - (Get-Content ./packers/choco/pode.nuspec) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/pode.nuspec - (Get-Content ./packers/choco/tools/ChocolateyInstall.ps1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/tools/ChocolateyInstall.ps1 + (Get-Content ./packers/choco/pode_template.nuspec) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/pode.nuspec + (Get-Content ./packers/choco/tools/ChocolateyInstall_template.ps1) | ForEach-Object { $_ -replace '\$version\$', $Version } | Set-Content ./packers/choco/tools/ChocolateyInstall.ps1 } # Synopsis: Generating a Checksum of the Zip -task PrintChecksum { - if (Test-PodeBuildIsWindows) { - $Script:Checksum = (checksum -t sha256 $Version-Binaries.zip) - } - else { - $Script:Checksum = (shasum -a 256 ./$Version-Binaries.zip | awk '{ print $1 }').ToUpper() - } - +Task PrintChecksum { + $Script:Checksum = (Get-FileHash "./deliverable/$Version-Binaries.zip" -Algorithm SHA256).Hash Write-Host "Checksum: $($Checksum)" } @@ -130,7 +156,7 @@ task PrintChecksum { #> # Synopsis: Installs Chocolatey -task ChocoDeps -If (Test-PodeBuildIsWindows) { +Task ChocoDeps -If (Test-PodeBuildIsWindows) { if (!(Test-PodeBuildCommand 'choco')) { Set-ExecutionPolicy Bypass -Scope Process -Force Invoke-Expression ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) @@ -138,26 +164,28 @@ task ChocoDeps -If (Test-PodeBuildIsWindows) { } # Synopsis: Install dependencies for packaging -task PackDeps -If (Test-PodeBuildIsWindows) ChocoDeps, { - if (!(Test-PodeBuildCommand 'checksum')) { - Invoke-PodeBuildInstall 'checksum' $Versions.Checksum - } - +Task PackDeps -If (Test-PodeBuildIsWindows) ChocoDeps, { if (!(Test-PodeBuildCommand '7z')) { Invoke-PodeBuildInstall '7zip' $Versions.SevenZip } } # Synopsis: Install dependencies for compiling/building -task BuildDeps { +Task BuildDeps { # install dotnet + if (Test-PodeBuildIsWindows) { + $dotnet = 'dotnet' + } + else { + $dotnet = "dotnet-sdk-$($Versions.DotNet)" + } if (!(Test-PodeBuildCommand 'dotnet')) { - Invoke-PodeBuildInstall 'dotnet' $Versions.DotNet + Invoke-PodeBuildInstall $dotnet $Versions.DotNet } } # Synopsis: Install dependencies for running tests -task TestDeps { +Task TestDeps { # install pester Install-PodeBuildModule Pester @@ -168,7 +196,7 @@ task TestDeps { } # Synopsis: Install dependencies for documentation -task DocsDeps ChocoDeps, { +Task DocsDeps ChocoDeps, { # install mkdocs if (!(Test-PodeBuildCommand 'mkdocs')) { Invoke-PodeBuildInstall 'mkdocs' $Versions.MkDocs @@ -189,17 +217,19 @@ task DocsDeps ChocoDeps, { #> # Synopsis: Build the .NET Listener -task Build BuildDeps, { +Task Build BuildDeps, { if (Test-Path ./src/Libs) { Remove-Item -Path ./src/Libs -Recurse -Force | Out-Null } - + Write-Host 'Powershell Version:' + $PSVersionTable.PSVersion Push-Location ./src/Listener try { - Invoke-PodeBuildDotnetBuild -target 'netstandard2.0' - Invoke-PodeBuildDotnetBuild -target 'net6.0' - Invoke-PodeBuildDotnetBuild -target 'net7.0' + Invoke-PodeBuildDotnetBuild -target 'netstandard2.0' -Version $Version + Invoke-PodeBuildDotnetBuild -target 'net6.0' -Version $Version + Invoke-PodeBuildDotnetBuild -target 'net7.0' -Version $Version + Invoke-PodeBuildDotnetBuild -target 'net8.0' -Version $Version } finally { Pop-Location @@ -212,17 +242,39 @@ task Build BuildDeps, { #> # Synopsis: Creates a Zip of the Module -task 7Zip -If (Test-PodeBuildIsWindows) PackDeps, StampVersion, { +Task 7Zip -If (Test-PodeBuildIsWindows) PackDeps, StampVersion, { exec { & 7z -tzip a $Version-Binaries.zip ./pkg/* } }, PrintChecksum + +# Synopsis: Creates a Zip of the Module +Task Compress StampVersion, { + $path = './deliverable' + if (Test-Path $path) { + Remove-Item -Path $path -Recurse -Force | Out-Null + } + # create the pkg dir + New-Item -Path $path -ItemType Directory -Force | Out-Null + Compress-Archive -Path './pkg' -DestinationPath "$path/$Version-Binaries.zip" +}, PrintChecksum + # Synopsis: Creates a Chocolately package of the Module -task ChocoPack -If (Test-PodeBuildIsWindows) PackDeps, StampVersion, { +Task ChocoPack -If (Test-PodeBuildIsWindows) PackDeps, StampVersion, { exec { choco pack ./packers/choco/pode.nuspec } + Move-Item -Path "pode.$Version.nupkg" -Destination './deliverable' } # Synopsis: Create docker tags -task DockerPack -If ((Test-PodeBuildIsWindows) -or $IsLinux) { +Task DockerPack -If (((Test-PodeBuildIsWindows) -or $IsLinux) ) { + try { + # Try to get the Docker version to check if Docker is installed + docker --version + } + catch { + # If Docker is not available, exit the task + Write-Warning 'Docker is not installed or not available in the PATH. Exiting task.' + return + } docker build -t badgerati/pode:$Version -f ./Dockerfile . docker build -t badgerati/pode:latest -f ./Dockerfile . docker build -t badgerati/pode:$Version-alpine -f ./alpine.dockerfile . @@ -239,12 +291,11 @@ task DockerPack -If ((Test-PodeBuildIsWindows) -or $IsLinux) { } # Synopsis: Package up the Module -task Pack -If (Test-PodeBuildIsWindows) Build, { +Task Pack Build, { $path = './pkg' if (Test-Path $path) { Remove-Item -Path $path -Recurse -Force | Out-Null } - # create the pkg dir New-Item -Path $path -ItemType Directory -Force | Out-Null @@ -263,7 +314,7 @@ task Pack -If (Test-PodeBuildIsWindows) Build, { Copy-Item -Path ./src/Pode.Internal.psm1 -Destination $path -Force | Out-Null Copy-Item -Path ./src/Pode.Internal.psd1 -Destination $path -Force | Out-Null Copy-Item -Path ./LICENSE.txt -Destination $path -Force | Out-Null -}, StampVersion, 7Zip, ChocoPack, DockerPack +}, StampVersion, Compress, ChocoPack, DockerPack <# @@ -271,7 +322,7 @@ task Pack -If (Test-PodeBuildIsWindows) Build, { #> # Synopsis: Run the tests -task Test Build, TestDeps, { +Task Test Build, TestDeps, { $p = (Get-Command Invoke-Pester) if ($null -eq $p -or $p.Version -ine $Versions.Pester) { Remove-Module Pester -Force -ErrorAction Ignore @@ -279,26 +330,35 @@ task Test Build, TestDeps, { } $Script:TestResultFile = "$($pwd)/TestResults.xml" - + # get default from static property + $configuration = [PesterConfiguration]::Default + $configuration.run.path = @('./tests/unit', './tests/integration') + $configuration.run.PassThru = $true + $configuration.TestResult.OutputFormat = 'NUnitXml' + $configuration.Output.Verbosity = $PesterVerbosity + $configuration.TestResult.OutputPath = $Script:TestResultFile # if run code coverage if enabled if (Test-PodeBuildCanCodeCoverage) { $srcFiles = (Get-ChildItem "$($pwd)/src/*.ps1" -Recurse -Force).FullName - $Script:TestStatus = Invoke-Pester './tests/unit', './tests/integration' -OutputFormat NUnitXml -OutputFile $TestResultFile -CodeCoverage $srcFiles -PassThru + $configuration.CodeCoverage.Enabled = $true + $configuration.CodeCoverage.Path = $srcFiles + $Script:TestStatus = Invoke-Pester -Configuration $configuration } else { - $Script:TestStatus = Invoke-Pester './tests/unit', './tests/integration' -OutputFormat NUnitXml -OutputFile $TestResultFile -Show Failed -PassThru + $configuration.Output.Verbosity = 'Detailed' + $Script:TestStatus = Invoke-Pester -Configuration $configuration } }, PushCodeCoverage, CheckFailedTests # Synopsis: Check if any of the tests failed -task CheckFailedTests { +Task CheckFailedTests { if ($TestStatus.FailedCount -gt 0) { throw "$($TestStatus.FailedCount) tests failed" } } # Synopsis: If AppyVeyor or GitHub, push code coverage stats -task PushCodeCoverage -If (Test-PodeBuildCanCodeCoverage) { +Task PushCodeCoverage -If (Test-PodeBuildCanCodeCoverage) { try { $service = Get-PodeBuildService $branch = Get-PodeBuildBranch @@ -318,12 +378,12 @@ task PushCodeCoverage -If (Test-PodeBuildCanCodeCoverage) { #> # Synopsis: Run the documentation locally -task Docs DocsDeps, DocsHelpBuild, { +Task Docs DocsDeps, DocsHelpBuild, { mkdocs serve } # Synopsis: Build the function help documentation -task DocsHelpBuild DocsDeps, { +Task DocsHelpBuild DocsDeps, { # import the local module Remove-Module Pode -Force -ErrorAction Ignore | Out-Null Import-Module ./src/Pode.psm1 -Force | Out-Null @@ -367,6 +427,99 @@ task DocsHelpBuild DocsDeps, { } # Synopsis: Build the documentation -task DocsBuild DocsDeps, DocsHelpBuild, { +Task DocsBuild DocsDeps, DocsHelpBuild, { mkdocs build +} + + +Task Clean { + $path = './deliverable' + if (Test-Path -Path $path -PathType Container) { + Remove-Item -Path $path -Recurse -Force | Out-Null + } + + $path = './pkg' + + if ((Test-Path -Path $path -PathType Container )) { + Remove-Item -Path $path -Recurse -Force | Out-Null + } + + if ((Test-Path -Path .\packers\choco\tools\ChocolateyInstall.ps1 -PathType Leaf )) { + Remove-Item -Path .\packers\choco\tools\ChocolateyInstall.ps1 + } + if ((Test-Path -Path .\packers\choco\pode.nuspec -PathType Leaf )) { + Remove-Item -Path .\packers\choco\pode.nuspec + } + Write-Host "$path Cleanup done" +} + +Task Install-Module { + + $path = './pkg' + + if ($Version) { + + if (! (Test-Path $path)) { + Invoke-Build Pack -Version $Version + } + if ($IsWindows -or (($PSVersionTable.Keys -contains 'PSEdition') -and ($PSVersionTable.PSEdition -eq 'Desktop'))) { + $PSPaths = $ENV:PSModulePath -split ';' + } + else { + $PSPaths = $ENV:PSModulePath -split ':' + } + $dest = join-path -Path $PSPaths[0] -ChildPath 'Pode' -AdditionalChildPath "$Version" + + if (Test-Path $dest) { + Remove-Item -Path $dest -Recurse -Force | Out-Null + } + + # create the dest dir + New-Item -Path $dest -ItemType Directory -Force | Out-Null + + Copy-Item -Path (Join-Path -Path $path -ChildPath 'Private' ) -Destination $dest -Force -Recurse | Out-Null + Copy-Item -Path (Join-Path -Path $path -ChildPath 'Public' ) -Destination $dest -Force -Recurse | Out-Null + Copy-Item -Path (Join-Path -Path $path -ChildPath 'Misc' ) -Destination $dest -Force -Recurse | Out-Null + Copy-Item -Path (Join-Path -Path $path -ChildPath 'Libs' ) -Destination $dest -Force -Recurse | Out-Null + + # copy general files + Copy-Item -Path $(Join-Path -Path $path -ChildPath 'Pode.psm1') -Destination $dest -Force | Out-Null + Copy-Item -Path $(Join-Path -Path $path -ChildPath 'Pode.psd1') -Destination $dest -Force | Out-Null + Copy-Item -Path $(Join-Path -Path $path -ChildPath 'Pode.Internal.psm1') -Destination $dest -Force | Out-Null + Copy-Item -Path $(Join-Path -Path $path -ChildPath 'Pode.Internal.psd1') -Destination $dest -Force | Out-Null + Copy-Item -Path $(Join-Path -Path $path -ChildPath 'LICENSE.txt') -Destination $dest -Force | Out-Null + + Write-Host "Deployed to $dest" + } + else { + Write-Error 'Parameter -Version is required' + } + +} + + +Task Remove-Module { + + if ($Version) { + if ($IsWindows -or (($PSVersionTable.Keys -contains 'PSEdition') -and ($PSVersionTable.PSEdition -eq 'Desktop'))) { + $PSPaths = $ENV:PSModulePath -split ';' + } + else { + $PSPaths = $ENV:PSModulePath -split ':' + } + $dest = join-path -Path $PSPaths[0] -ChildPath 'Pode' -AdditionalChildPath "$Version" + + if (Test-Path $dest) { + Write-Host "Deleting module from $dest" + Remove-Item -Path $dest -Recurse -Force | Out-Null + } + else { + Write-Error "Directory $dest doesn't exist" + } + + } + else { + Write-Error 'Parameter -Version is required' + } + } \ No newline at end of file diff --git a/src/Listener/Pode.csproj b/src/Listener/Pode.csproj index f65cedc7f..ac6f33723 100644 --- a/src/Listener/Pode.csproj +++ b/src/Listener/Pode.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net6.0;net7.0 + netstandard2.0;net6.0;net7.0;net8.0 $(NoWarn);SYSLIB0001 From 7ea2c3f0dc69a53b01807c00613b65fafd79af38 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 11:06:28 -0700 Subject: [PATCH 32/84] Migration to Pester 5.5 #1260 Migration to Pester 5.5 --- pode.build.ps1 | 2 +- tests/integration/Authentication.Tests.ps1 | 72 +- tests/integration/Endpoints.Tests.ps1 | 19 +- tests/integration/RestApi.Https.Tests.ps1 | 87 +- tests/integration/RestApi.Tests.ps1 | 66 +- tests/integration/Schedules.Tests.ps1 | 9 +- tests/integration/Sessions.Tests.ps1 | 48 +- tests/integration/Timers.Tests.ps1 | 7 +- tests/integration/WebPages.Tests.ps1 | 28 +- tests/unit/Authentication.Tests.ps1 | 81 +- tests/unit/Context.Tests.ps1 | 479 ++--- tests/unit/Cookies.Tests.ps1 | 285 +-- tests/unit/CronParser.Tests.ps1 | 571 +++--- tests/unit/Cryptography.Tests.ps1 | 28 +- tests/unit/Endware.Tests.ps1 | 20 +- tests/unit/Flash.Tests.ps1 | 137 +- tests/unit/Handlers.Tests.ps1 | 110 +- tests/unit/Headers.Tests.ps1 | 169 +- tests/unit/Helpers.Tests.ps1 | 901 +++++---- tests/unit/Logging.Tests.ps1 | 116 +- tests/unit/Mappers.Tests.ps1 | 2126 +++++++++++++------- tests/unit/Metrics.Tests.ps1 | 57 +- tests/unit/Middleware.Tests.ps1 | 614 +++--- tests/unit/NameGenerator.Tests.ps1 | 13 +- tests/unit/Responses.Tests.ps1 | 321 +-- tests/unit/Routes.Tests.ps1 | 748 +++---- tests/unit/Schedules.Tests.ps1 | 183 +- tests/unit/Security.Tests.ps1 | 463 +++-- tests/unit/Server.Tests.ps1 | 118 +- tests/unit/Serverless.Tests.ps1 | 154 +- tests/unit/Sessions.Tests.ps1 | 84 +- tests/unit/State.Tests.ps1 | 43 +- tests/unit/Timers.Tests.ps1 | 111 +- tests/unit/_.Tests.ps1 | 13 +- 34 files changed, 4641 insertions(+), 3642 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index a0366fd10..296735d02 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -8,7 +8,7 @@ param( #> $Versions = @{ - Pester = '4.8.0' + Pester = '5.5.0' MkDocs = '1.5.3' PSCoveralls = '1.0.0' SevenZip = '18.5.0.20180730' diff --git a/tests/integration/Authentication.Tests.ps1 b/tests/integration/Authentication.Tests.ps1 index aec288241..27b62e21f 100644 --- a/tests/integration/Authentication.Tests.ps1 +++ b/tests/integration/Authentication.Tests.ps1 @@ -1,6 +1,12 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]integration', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]integration', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Authentication Requests' { @@ -11,7 +17,7 @@ Describe 'Authentication Requests' { Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer { + Start-PodeServer -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging @@ -24,7 +30,7 @@ Describe 'Authentication Requests' { param($username, $password) if (($username -eq 'morty') -and ($password -eq 'pickle')) { - return @{ User = @{ ID ='M0R7Y302' } } + return @{ User = @{ ID = 'M0R7Y302' } } } return @{ Message = 'Invalid details supplied' } @@ -40,7 +46,7 @@ Describe 'Authentication Requests' { if ($token -ieq 'test-token') { return @{ - User = @{ ID ='M0R7Y302' } + User = @{ ID = 'M0R7Y302' } Scope = 'write' } } @@ -58,7 +64,7 @@ Describe 'Authentication Requests' { if ($key -ieq 'test-key') { return @{ - User = @{ ID ='M0R7Y302' } + User = @{ ID = 'M0R7Y302' } } } @@ -75,7 +81,7 @@ Describe 'Authentication Requests' { if ($jwt.username -ieq 'morty') { return @{ - User = @{ ID ='M0R7Y302' } + User = @{ ID = 'M0R7Y302' } } } @@ -92,7 +98,7 @@ Describe 'Authentication Requests' { if ($jwt.username -ieq 'morty') { return @{ - User = @{ ID ='M0R7Y302' } + User = @{ ID = 'M0R7Y302' } } } @@ -120,45 +126,45 @@ Describe 'Authentication Requests' { # BASIC It 'basic - returns ok for valid creds' { $result = Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'basic - returns 401 for invalid creds' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic cmljazpwaWNrbGU=' } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic cmljazpwaWNrbGU=' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } It 'basic - returns 400 for invalid base64' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic cmlazpwaNrbGU' } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic cmlazpwaNrbGU' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } # BEARER It 'bearer - returns ok for valid token' { $result = Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer test-token' } - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'bearer - returns 401 for invalid token' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer fake-token' } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer fake-token' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } It 'bearer - returns 400 for no token' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer' } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/bearer" -Method Get -Headers @{ Authorization = 'Bearer' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } # API KEY It 'apikey - returns ok for valid key' { $result = Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey" -Method Get -Headers @{ 'X-API-KEY' = 'test-key' } - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'apikey - returns 401 for invalid key' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey" -Method Get -Headers @{ 'X-API-KEY' = 'fake-key' } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey" -Method Get -Headers @{ 'X-API-KEY' = 'fake-key' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } It 'apikey - returns 400 for no key' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey" -Method Get -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey" -Method Get -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } @@ -169,7 +175,7 @@ Describe 'Authentication Requests' { $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload $result = Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'apikey - jwt not signed - returns 400 for invalid key - invalid base64' { @@ -177,7 +183,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = "hh$($jwt)" } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = "hh$($jwt)" } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } It 'apikey - jwt not signed - returns 401 for invalid key - invalid username' { @@ -185,7 +191,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'rick' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } It 'apikey - jwt not signed - returns 400 for invalid key - expired' { @@ -193,7 +199,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty'; exp = 100 } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } It 'apikey - jwt not signed - returns 400 for invalid key - not started' { @@ -201,7 +207,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty'; nbf = ([System.DateTimeOffset]::Now.AddYears(1).ToUnixTimeSeconds()) } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/notsigned" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } @@ -212,21 +218,21 @@ Describe 'Authentication Requests' { $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'secret' $result = Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'apikey - jwt signed - returns ok for valid key - valid exp/nbf' { $header = @{ alg = 'hs256' } $payload = @{ - sub = '123' + sub = '123' username = 'morty' - nbf = ([System.DateTimeOffset]::Now.AddDays(-1).ToUnixTimeSeconds()) - exp = ([System.DateTimeOffset]::Now.AddDays(1).ToUnixTimeSeconds()) + nbf = ([System.DateTimeOffset]::Now.AddDays(-1).ToUnixTimeSeconds()) + exp = ([System.DateTimeOffset]::Now.AddDays(1).ToUnixTimeSeconds()) } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'secret' $result = Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'apikey - jwt signed - returns 400 for invalid key - invalid base64' { @@ -234,7 +240,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'secret' - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = "hh$($jwt)" } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = "hh$($jwt)" } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } It 'apikey - jwt signed - returns 400 for invalid key - invalid signature' { @@ -242,7 +248,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'secret' - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = "$($jwt)hh" } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = "$($jwt)hh" } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } It 'apikey - jwt signed - returns 400 for invalid key - invalid secret' { @@ -250,7 +256,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'fake' - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } It 'apikey - jwt signed - returns 400 for invalid key - none algorithm' { @@ -258,7 +264,7 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'morty' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should Throw '400' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*400*' } It 'apikey - jwt signed - returns 401 for invalid key - invalid username' { @@ -266,6 +272,6 @@ Describe 'Authentication Requests' { $payload = @{ sub = '123'; username = 'rick' } $jwt = ConvertTo-PodeJwt -Header $header -Payload $payload -Secret 'secret' - { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/apikey/jwt/signed" -Method Get -Headers @{ 'X-API-KEY' = $jwt } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } } \ No newline at end of file diff --git a/tests/integration/Endpoints.Tests.ps1 b/tests/integration/Endpoints.Tests.ps1 index e7f33a457..d9dc71261 100644 --- a/tests/integration/Endpoints.Tests.ps1 +++ b/tests/integration/Endpoints.Tests.ps1 @@ -1,3 +1,6 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + Describe 'Endpoint Requests' { BeforeAll { @@ -10,12 +13,12 @@ Describe 'Endpoint Requests' { Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer -RootPath $using:PSScriptRoot { + Start-PodeServer -RootPath $using:PSScriptRoot -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port1 -Protocol Http -Name 'Endpoint1' Add-PodeEndpoint -Address localhost -Port $using:Port2 -Protocol Http -Name 'Endpoint2' New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging - Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { Close-PodeServer } @@ -45,26 +48,26 @@ Describe 'Endpoint Requests' { It 'responds back with pong1' { $result = Invoke-RestMethod -Uri "$($Endpoint1)/ping-1" -Method Get - $result.Result | Should Be 'Pong1' + $result.Result | Should -Be 'Pong1' } It 'fails pong1 on second endpoint' { - { Invoke-RestMethod -Uri "$($Endpoint2)/ping-1" -Method Get -ErrorAction Stop } | Should Throw '404' + { Invoke-RestMethod -Uri "$($Endpoint2)/ping-1" -Method Get -ErrorAction Stop } | Should -Throw -ExpectedMessage '*404*' } It 'responds back with pong2' { $result = Invoke-RestMethod -Uri "$($Endpoint2)/ping-2" -Method Get - $result.Result | Should Be 'Pong2' + $result.Result | Should -Be 'Pong2' } It 'fails pong2 on first endpoint' { - { Invoke-RestMethod -Uri "$($Endpoint1)/ping-2" -Method Get -ErrorAction Stop } | Should Throw '404' + { Invoke-RestMethod -Uri "$($Endpoint1)/ping-2" -Method Get -ErrorAction Stop } | Should -Throw -ExpectedMessage '*404*' } It 'responds back with pong all' { $result = Invoke-RestMethod -Uri "$($Endpoint1)/ping-all" -Method Get - $result.Result | Should Be 'PongAll' + $result.Result | Should -Be 'PongAll' $result = Invoke-RestMethod -Uri "$($Endpoint2)/ping-all" -Method Get - $result.Result | Should Be 'PongAll' + $result.Result | Should -Be 'PongAll' } } \ No newline at end of file diff --git a/tests/integration/RestApi.Https.Tests.ps1 b/tests/integration/RestApi.Https.Tests.ps1 index 868fed7a9..b4cf50f2e 100644 --- a/tests/integration/RestApi.Https.Tests.ps1 +++ b/tests/integration/RestApi.Https.Tests.ps1 @@ -1,8 +1,13 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + Describe 'REST API Requests' { - $splatter = @{} + BeforeAll { + $splatter = @{} - if ($PSVersionTable.PSVersion.Major -le 5) { - Add-Type @" + if ($PSVersionTable.PSVersion.Major -le 5) { + Add-Type @' using System.Net; using System.Security.Cryptography.X509Certificates; public class TrustAllCertsPolicy : ICertificatePolicy { @@ -12,14 +17,14 @@ Describe 'REST API Requests' { return true; } } -"@ - [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - } - else { - $splatter.SkipCertificateCheck = $true - } +'@ + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + } + else { + $splatter.SkipCertificateCheck = $true + } + - BeforeAll { $Port = 50010 $Endpoint = "https://localhost:$($Port)" @@ -30,7 +35,7 @@ Describe 'REST API Requests' { Write-PodeJsonResponse -Value @{ Message = 'Outer Hello' } } - Start-PodeServer -RootPath $using:PSScriptRoot { + Start-PodeServer -RootPath $using:PSScriptRoot -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Https -SelfSigned New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging @@ -89,11 +94,11 @@ Describe 'REST API Requests' { } Add-PodeRoute -Method * -Path '/all' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Result ='OK' } + Write-PodeJsonResponse -Value @{ Result = 'OK' } } Add-PodeRoute -Method Get -Path '/api/*/hello' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Result ='OK' } + Write-PodeJsonResponse -Value @{ Result = 'OK' } } Add-PodeRoute -Method Get -Path '/imported/func/outer' -ScriptBlock { @@ -123,66 +128,66 @@ Describe 'REST API Requests' { It 'responds back with pong' { $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter - $result.Result | Should Be 'Pong' + $result.Result | Should -Be 'Pong' } It 'responds back with 404 for invalid route' { - { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should Throw '404' + { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' } It 'responds back with 405 for incorrect method' { - { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should Throw '405' + { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' } It 'responds with simple query parameter' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - json' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - xml' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter forced to json' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple route parameter' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple route parameter long' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter - $result.Messages[0] | Should Be 'Hello, world!' - $result.Messages[1] | Should Be 'Greetings' - $result.Messages[2] | Should Be 'Wubba Lub' + $result.Messages[0] | Should -Be 'Hello, world!' + $result.Messages[1] | Should -Be 'Greetings' + $result.Messages[2] | Should -Be 'Wubba Lub' } It 'responds ok to remove account' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'responds ok to replace account' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'responds ok to update account' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'decodes encoded payload parameter - gzip' { - $data = @{ username = "rick" } + $data = @{ username = 'rick' } $message = ($data | ConvertTo-Json) # compress the message using gzip @@ -195,11 +200,11 @@ Describe 'REST API Requests' { # make the request $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'decodes encoded payload parameter - deflate' { - $data = @{ username = "rick" } + $data = @{ username = 'rick' } $message = ($data | ConvertTo-Json) # compress the message using deflate @@ -212,11 +217,11 @@ Describe 'REST API Requests' { # make the request $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'decodes encoded payload parameter forced to gzip' { - $data = @{ username = "rick" } + $data = @{ username = 'rick' } $message = ($data | ConvertTo-Json) # compress the message using gzip @@ -229,38 +234,38 @@ Describe 'REST API Requests' { # make the request $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' @splatter - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'works with any method' { $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'route with a wild card' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'route importing outer function' { $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter - $result.Message | Should Be 'Outer Hello' + $result.Message | Should -Be 'Outer Hello' } It 'route importing outer function' { $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter - $result.Message | Should Be 'Inner Hello' + $result.Message | Should -Be 'Inner Hello' } } \ No newline at end of file diff --git a/tests/integration/RestApi.Tests.ps1 b/tests/integration/RestApi.Tests.ps1 index 3d679d7d3..5a270bb63 100644 --- a/tests/integration/RestApi.Tests.ps1 +++ b/tests/integration/RestApi.Tests.ps1 @@ -1,3 +1,7 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + Describe 'REST API Requests' { BeforeAll { @@ -11,7 +15,7 @@ Describe 'REST API Requests' { Write-PodeJsonResponse -Value @{ Message = 'Outer Hello' } } - Start-PodeServer -RootPath $using:PSScriptRoot { + Start-PodeServer -RootPath $using:PSScriptRoot -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging @@ -70,11 +74,11 @@ Describe 'REST API Requests' { } Add-PodeRoute -Method * -Path '/all' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Result ='OK' } + Write-PodeJsonResponse -Value @{ Result = 'OK' } } Add-PodeRoute -Method Get -Path '/api/*/hello' -ScriptBlock { - Write-PodeJsonResponse -Value @{ Result ='OK' } + Write-PodeJsonResponse -Value @{ Result = 'OK' } } Add-PodeRoute -Method Get -Path '/imported/func/outer' -ScriptBlock { @@ -99,66 +103,66 @@ Describe 'REST API Requests' { It 'responds back with pong' { $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get - $result.Result | Should Be 'Pong' + $result.Result | Should -Be 'Pong' } It 'responds back with 404 for invalid route' { - { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop } | Should Throw '404' + { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop } | Should -Throw -ExpectedMessage '*404*' } It 'responds back with 405 for incorrect method' { - { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop } | Should Throw '405' + { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop } | Should -Throw -ExpectedMessage '*405*' } It 'responds with simple query parameter' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - json' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - xml' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter forced to json' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple route parameter' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'responds with simple route parameter long' { $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get - $result.Messages[0] | Should Be 'Hello, world!' - $result.Messages[1] | Should Be 'Greetings' - $result.Messages[2] | Should Be 'Wubba Lub' + $result.Messages[0] | Should -Be 'Hello, world!' + $result.Messages[1] | Should -Be 'Greetings' + $result.Messages[2] | Should -Be 'Wubba Lub' } It 'responds ok to remove account' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'responds ok to replace account' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'responds ok to update account' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'decodes encoded payload parameter - gzip' { - $data = @{ username = "rick" } + $data = @{ username = 'rick' } $message = ($data | ConvertTo-Json) # compress the message using gzip @@ -171,11 +175,11 @@ Describe 'REST API Requests' { # make the request $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'decodes encoded payload parameter - deflate' { - $data = @{ username = "rick" } + $data = @{ username = 'rick' } $message = ($data | ConvertTo-Json) # compress the message using deflate @@ -188,11 +192,11 @@ Describe 'REST API Requests' { # make the request $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'decodes encoded payload parameter forced to gzip' { - $data = @{ username = "rick" } + $data = @{ username = 'rick' } $message = ($data | ConvertTo-Json) # compress the message using gzip @@ -205,38 +209,38 @@ Describe 'REST API Requests' { # make the request $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' - $result.Username | Should Be 'rick' + $result.Username | Should -Be 'rick' } It 'works with any method' { $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'route with a wild card' { $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get - $result.Result | Should Be 'OK' + $result.Result | Should -Be 'OK' } It 'route importing outer function' { $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get - $result.Message | Should Be 'Outer Hello' + $result.Message | Should -Be 'Outer Hello' } It 'route importing outer function' { $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get - $result.Message | Should Be 'Inner Hello' + $result.Message | Should -Be 'Inner Hello' } } \ No newline at end of file diff --git a/tests/integration/Schedules.Tests.ps1 b/tests/integration/Schedules.Tests.ps1 index 241a80fbe..fd6c5fdef 100644 --- a/tests/integration/Schedules.Tests.ps1 +++ b/tests/integration/Schedules.Tests.ps1 @@ -1,3 +1,6 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + Describe 'Schedules' { BeforeAll { @@ -7,7 +10,7 @@ Describe 'Schedules' { Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer -RootPath $using:PSScriptRoot { + Start-PodeServer -RootPath $using:PSScriptRoot -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging @@ -53,11 +56,11 @@ Describe 'Schedules' { It 'schedule updates state value - full cron' { $result = Invoke-RestMethod -Uri "$($Endpoint)/test1" -Method Get - $result.Result | Should Be 1337 + $result.Result | Should -Be 1337 } It 'schedule updates state value - short cron' { $result = Invoke-RestMethod -Uri "$($Endpoint)/test2" -Method Get - $result.Result | Should Be 314 + $result.Result | Should -Be 314 } } \ No newline at end of file diff --git a/tests/integration/Sessions.Tests.ps1 b/tests/integration/Sessions.Tests.ps1 index bfa4dd6e1..4055e2209 100644 --- a/tests/integration/Sessions.Tests.ps1 +++ b/tests/integration/Sessions.Tests.ps1 @@ -1,3 +1,7 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '')] +param() + Describe 'Session Requests' { BeforeAll { @@ -7,7 +11,7 @@ Describe 'Session Requests' { Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer { + Start-PodeServer -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { Close-PodeServer @@ -19,7 +23,7 @@ Describe 'Session Requests' { param($username, $password) if (($username -eq 'morty') -and ($password -eq 'pickle')) { - return @{ User = @{ ID ='M0R7Y302' } } + return @{ User = @{ ID = 'M0R7Y302' } } } return @{ Message = 'Invalid details supplied' } @@ -29,9 +33,9 @@ Describe 'Session Requests' { $WebEvent.Session.Data.Views++ Write-PodeJsonResponse -Value @{ - Result = 'OK' + Result = 'OK' Username = $WebEvent.Auth.User.ID - Views = $WebEvent.Session.Data.Views + Views = $WebEvent.Session.Data.Views } } } @@ -51,57 +55,57 @@ Describe 'Session Requests' { $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } $content = ($result.Content | ConvertFrom-Json) - $content.Result | Should Be 'OK' - $content.Views | Should Be 1 - $result.Headers['pode.sid'] | Should Not Be $null + $content.Result | Should -Be 'OK' + $content.Views | Should -Be 1 + $result.Headers['pode.sid'] | Should -Not -BeNullOrEmpty } It 'returns 401 for invalid creds' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic cmljazpwaWNrbGU=' } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic cmljazpwaWNrbGU=' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } It 'returns ok for session requests' { $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } $content = ($result.Content | ConvertFrom-Json) - $content.Result | Should Be 'OK' - $content.Views | Should Be 1 - $result.Headers['pode.sid'] | Should Not Be $null + $content.Result | Should -Be 'OK' + $content.Views | Should -Be 1 + $result.Headers['pode.sid'] | Should -Not -BeNullOrEmpty $session = ($result.Headers['pode.sid'] | Select-Object -First 1) $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } $content = ($result.Content | ConvertFrom-Json) - $content.Result | Should Be 'OK' - $content.Views | Should Be 2 + $content.Result | Should -Be 'OK' + $content.Views | Should -Be 2 $session = ($result.Headers['pode.sid'] | Select-Object -First 1) $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } $content = ($result.Content | ConvertFrom-Json) - $content.Result | Should Be 'OK' - $content.Views | Should Be 3 + $content.Result | Should -Be 'OK' + $content.Views | Should -Be 3 } It 'returns 401 for invalid session' { - { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = 'some-fake-session' } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = 'some-fake-session' } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } It 'returns 401 for session timeout' { $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ Authorization = 'Basic bW9ydHk6cGlja2xl' } $content = ($result.Content | ConvertFrom-Json) - $content.Result | Should Be 'OK' - $content.Views | Should Be 1 - $result.Headers['pode.sid'] | Should Not Be $null + $content.Result | Should -Be 'OK' + $content.Views | Should -Be 1 + $result.Headers['pode.sid'] | Should -Not -BeNullOrEmpty $session = ($result.Headers['pode.sid'] | Select-Object -First 1) $result = Invoke-WebRequest -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } $content = ($result.Content | ConvertFrom-Json) - $content.Result | Should Be 'OK' - $content.Views | Should Be 2 + $content.Result | Should -Be 'OK' + $content.Views | Should -Be 2 Start-Sleep -Seconds 6 $session = ($result.Headers['pode.sid'] | Select-Object -First 1) - { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } -ErrorAction Stop } | Should Throw '401' + { Invoke-RestMethod -Uri "$($Endpoint)/auth/basic" -Method Post -Headers @{ 'pode.sid' = $session } -ErrorAction Stop } | Should -Throw -ExpectedMessage '*401*' } } \ No newline at end of file diff --git a/tests/integration/Timers.Tests.ps1 b/tests/integration/Timers.Tests.ps1 index cd4b50d91..d251b430e 100644 --- a/tests/integration/Timers.Tests.ps1 +++ b/tests/integration/Timers.Tests.ps1 @@ -1,3 +1,6 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + Describe 'Timers' { BeforeAll { @@ -7,7 +10,7 @@ Describe 'Timers' { Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer -RootPath $using:PSScriptRoot { + Start-PodeServer -RootPath $using:PSScriptRoot -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging @@ -39,6 +42,6 @@ Describe 'Timers' { It 'timer updates state value' { $result = Invoke-RestMethod -Uri "$($Endpoint)/test1" -Method Get - $result.Result | Should Be 1337 + $result.Result | Should -Be 1337 } } \ No newline at end of file diff --git a/tests/integration/WebPages.Tests.ps1 b/tests/integration/WebPages.Tests.ps1 index bc85a2b8d..232f2c602 100644 --- a/tests/integration/WebPages.Tests.ps1 +++ b/tests/integration/WebPages.Tests.ps1 @@ -1,3 +1,5 @@ +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() Describe 'Web Page Requests' { BeforeAll { @@ -7,7 +9,7 @@ Describe 'Web Page Requests' { Start-Job -Name 'Pode' -ErrorAction Stop -ScriptBlock { Import-Module -Name "$($using:PSScriptRoot)\..\..\src\Pode.psm1" - Start-PodeServer -RootPath $using:PSScriptRoot { + Start-PodeServer -RootPath $using:PSScriptRoot -Quiet -ScriptBlock { Add-PodeEndpoint -Address localhost -Port $using:Port -Protocol Http Add-PodeRoute -Method Get -Path '/close' -ScriptBlock { Close-PodeServer @@ -46,40 +48,40 @@ Describe 'Web Page Requests' { It 'responds with a dynamic view' { $result = Invoke-WebRequest -Uri "$($Endpoint)/views/dynamic" -Method Get - $result.Content | Should Be '

2020-03-14

' + $result.Content | Should -Be '

2020-03-14

' } It 'responds with a static view' { $result = Invoke-WebRequest -Uri "$($Endpoint)/views/static" -Method Get - $result.Content | Should Be '

2020-01-01

' + $result.Content | Should -Be '

2020-01-01

' } It 'redirects you to another url' { $result = Invoke-WebRequest -Uri "$($Endpoint)/redirect" -Method Get - $result.StatusCode | Should Be 200 - $result.Content.Contains('google') | Should Be $true + $result.StatusCode | Should -Be 200 + $result.Content.Contains('google') | Should -Be $true } It 'attaches and image for download' { $result = Invoke-WebRequest -Uri "$($Endpoint)/attachment" -Method Get - $result.StatusCode | Should Be 200 - $result.Headers['Content-Type'] | Should Be 'image/png' - $result.Headers['Content-Disposition'] | Should Be 'attachment; filename=ruler.png' + $result.StatusCode | Should -Be 200 + $result.Headers['Content-Type'] | Should -Be 'image/png' + $result.Headers['Content-Disposition'] | Should -Be 'attachment; filename=ruler.png' } It 'responds with public static content' { $result = Invoke-WebRequest -Uri "$($Endpoint)/ruler.png" -Method Get - $result.StatusCode | Should Be 200 - $result.Headers['Content-Type'] | Should Be 'image/png; charset=utf-8' + $result.StatusCode | Should -Be 200 + $result.Headers['Content-Type'] | Should -Be 'image/png; charset=utf-8' } It 'responds with 404 for non-public static content' { - { Invoke-WebRequest -Uri "$($Endpoint)/images/custom_ruler.png" -Method Get -ErrorAction Stop } | Should Throw '404' + { Invoke-WebRequest -Uri "$($Endpoint)/images/custom_ruler.png" -Method Get -ErrorAction Stop } | Should -Throw -ExpectedMessage '*404*' } It 'responds with custom static content' { $result = Invoke-WebRequest -Uri "$($Endpoint)/custom-images/custom_ruler.png" -Method Get - $result.StatusCode | Should Be 200 - $result.Headers['Content-Type'] | Should Be 'image/png; charset=utf-8' + $result.StatusCode | Should -Be 200 + $result.Headers['Content-Type'] | Should -Be 'image/png; charset=utf-8' } } \ No newline at end of file diff --git a/tests/unit/Authentication.Tests.ps1 b/tests/unit/Authentication.Tests.ps1 index 764b3d5ae..ea67555aa 100644 --- a/tests/unit/Authentication.Tests.ps1 +++ b/tests/unit/Authentication.Tests.ps1 @@ -1,47 +1,52 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} $now = [datetime]::UtcNow Describe 'Set-PodeAuthStatus' { - Mock Move-PodeResponseUrl {} - Mock Set-PodeResponseStatus {} - - $PodeContext = @{ - Server = @{ - Authentications = @{ - Methods = @{ - ExampleAuth = @{ - Failure = @{ Url = '/url' } - Success = @{ Url = '/url' } - Cache = @{} + BeforeAll { + Mock Move-PodeResponseUrl {} + Mock Set-PodeResponseStatus {} + + $PodeContext = @{ + Server = @{ + Authentications = @{ + Methods = @{ + ExampleAuth = @{ + Failure = @{ Url = '/url' } + Success = @{ Url = '/url' } + Cache = @{} + } } } } - } - } + } } It 'Redirects to a failure URL' { - Set-PodeAuthStatus -StatusCode 500 -Name ExampleAuth | Should Be $false + Set-PodeAuthStatus -StatusCode 500 -Name ExampleAuth | Should -Be $false Assert-MockCalled Move-PodeResponseUrl -Times 1 -Scope It Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It } It 'Sets status to failure' { - Set-PodeAuthStatus -StatusCode 500 -Name ExampleAuth | Should Be $false + Set-PodeAuthStatus -StatusCode 500 -Name ExampleAuth | Should -Be $false Assert-MockCalled Move-PodeResponseUrl -Times 1 -Scope It Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It } It 'Redirects to a success URL' { - Set-PodeAuthStatus -Name ExampleAuth -LoginRoute | Should Be $false + Set-PodeAuthStatus -Name ExampleAuth -LoginRoute | Should -Be $false Assert-MockCalled Move-PodeResponseUrl -Times 1 -Scope It Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It } It 'Returns true for next middleware' { - Set-PodeAuthStatus -Name ExampleAuth -NoSuccessRedirect | Should Be $true + Set-PodeAuthStatus -Name ExampleAuth -NoSuccessRedirect | Should -Be $true Assert-MockCalled Move-PodeResponseUrl -Times 0 -Scope It Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It } @@ -50,32 +55,32 @@ Describe 'Set-PodeAuthStatus' { Describe 'Get-PodeAuthBasicType' { It 'Returns form auth type' { $result = Get-PodeAuthBasicType - $result | Should Not Be $null - $result.GetType().Name | Should Be 'ScriptBlock' + $result | Should -Not -Be $null + $result.GetType().Name | Should -Be 'ScriptBlock' } } Describe 'Get-PodeAuthFormType' { It 'Returns basic auth type' { $result = Get-PodeAuthFormType - $result | Should Not Be $null - $result.GetType().Name | Should Be 'ScriptBlock' + $result | Should -Not -Be $null + $result.GetType().Name | Should -Be 'ScriptBlock' } } Describe 'Get-PodeAuthInbuiltMethod' { It 'Returns Windows AD auth' { $result = Get-PodeAuthWindowsADMethod - $result | Should Not Be $null - $result.GetType().Name | Should Be 'ScriptBlock' + $result | Should -Not -Be $null + $result.GetType().Name | Should -Be 'ScriptBlock' } } Describe 'Get-PodeAuthMiddlewareScript' { It 'Returns auth middleware' { $result = Get-PodeAuthMiddlewareScript - $result | Should Not Be $null - $result.GetType().Name | Should Be 'ScriptBlock' + $result | Should -Not -Be $null + $result.GetType().Name | Should -Be 'ScriptBlock' } } @@ -84,7 +89,7 @@ Describe 'Remove-PodeAuthSession' { Mock Revoke-PodeSession {} $WebEvent = @{ - Auth = @{ User = @{} } + Auth = @{ User = @{} } Session = @{ Data = @{ Auth = @{ User = @{} } @@ -94,9 +99,9 @@ Describe 'Remove-PodeAuthSession' { Remove-PodeAuthSession - $WebEvent.Auth.Count | Should Be 0 - $WebEvent.Auth.User | Should Be $null - $WebEvent.Session.Data.Auth | Should be $null + $WebEvent.Auth.Count | Should -Be 0 + $WebEvent.Auth.User | Should -Be $null + $WebEvent.Session.Data.Auth | Should -Be $null Assert-MockCalled Revoke-PodeSession -Times 1 -Scope It } @@ -105,22 +110,22 @@ Describe 'Remove-PodeAuthSession' { Mock Revoke-PodeSession {} $WebEvent = @{ - Auth = @{ User = @{} } + Auth = @{ User = @{} } Session = @{ Data = @{ Auth = @{ User = @{} } } } Request = @{ - Url = @{ AbsolutePath ='/' } + Url = @{ AbsolutePath = '/' } } } Remove-PodeAuthSession - $WebEvent.Auth.Count | Should Be 0 - $WebEvent.Auth.User | Should Be $null - $WebEvent.Session.Data.Auth | Should be $null + $WebEvent.Auth.Count | Should -Be 0 + $WebEvent.Auth.User | Should -Be $null + $WebEvent.Session.Data.Auth | Should -Be $null Assert-MockCalled Revoke-PodeSession -Times 1 -Scope It } @@ -128,7 +133,7 @@ Describe 'Remove-PodeAuthSession' { Describe 'Test-PodeJwt' { It 'No exception - sucessful validation' { - (Test-PodeJwt @{}) | Should Be $null + (Test-PodeJwt @{}) | Should -Be $null } It 'Throws exception - the JWT has expired' { diff --git a/tests/unit/Context.Tests.ps1 b/tests/unit/Context.Tests.ps1 index 94e5f3adf..dc89568b1 100644 --- a/tests/unit/Context.Tests.ps1 +++ b/tests/unit/Context.Tests.ps1 @@ -1,237 +1,242 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + $PodeContext = @{ 'Server' = $null; } +} -$PodeContext = @{ 'Server' = $null; } Describe 'Get-PodeConfig' { It 'Returns JSON config' { $json = '{ "settings": { "port": 90 } }' $PodeContext.Server = @{ 'Configuration' = ($json | ConvertFrom-Json) } $config = Get-PodeConfig - $config | Should Not Be $null - $config.settings.port | Should Be 90 + $config | Should -Not -Be $null + $config.settings.port | Should -Be 90 } } Describe 'Add-PodeEndpoint' { Context 'Invalid parameters supplied' { It 'Throw invalid type error for no protocol' { - { Add-PodeEndpoint -Address '127.0.0.1' -Protocol 'MOO' } | Should Throw "Cannot validate argument on parameter 'Protocol'" + { Add-PodeEndpoint -Address '127.0.0.1' -Protocol 'MOO' } | Should -Throw -ExpectedMessage "Cannot validate argument on parameter 'Protocol'*" } } Context 'Valid parameters supplied' { - Mock Test-PodeIPAddress { return $true } - Mock Test-PodeIsAdminUser { return $true } - + BeforeAll { + Mock Test-PodeIPAddress { return $true } + Mock Test-PodeIsAdminUser { return $true } + } It 'Set just a Hostname address - old' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address 'foo.com' -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.Name | Should Not Be ([string]::Empty) - $endpoint.HostName | Should Be 'foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' - $endpoint.Hostname.ToString() | Should Be 'foo.com' - $endpoint.RawAddress | Should Be 'foo.com:8080' + $endpoint.Port | Should -Be 8080 + $endpoint.Name | Should -Not -Be ([string]::Empty) + $endpoint.HostName | Should -Be 'foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' + $endpoint.Hostname.ToString() | Should -Be 'foo.com' + $endpoint.RawAddress | Should -Be 'foo.com:8080' } It 'Set just a Hostname address - new' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Hostname 'foo.com' -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.Name | Should Not Be ([string]::Empty) - $endpoint.HostName | Should Be 'foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' - $endpoint.RawAddress | Should Be 'foo.com:8080' + $endpoint.Port | Should -Be 8080 + $endpoint.Name | Should -Not -Be ([string]::Empty) + $endpoint.HostName | Should -Be 'foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' + $endpoint.RawAddress | Should -Be 'foo.com:8080' } It 'Set Hostname address with a Name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address 'foo.com' -Protocol 'HTTP' -Name 'Example' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.Name | Should Be 'Example' - $endpoint.HostName | Should Be 'foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 8080 + $endpoint.Name | Should -Be 'Example' + $endpoint.HostName | Should -Be 'foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set just a Hostname address with colon' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address 'foo.com' -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.HostName | Should Be 'foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' - $endpoint.RawAddress | Should Be 'foo.com:8080' + $endpoint.Port | Should -Be 8080 + $endpoint.HostName | Should -Be 'foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' + $endpoint.RawAddress | Should -Be 'foo.com:8080' } It 'Set both the Hostname address and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address 'foo.com' -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be 'foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be 'foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set all the Hostname, ip and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.2' -Hostname 'foo.com' -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be 'foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.2' - $endpoint.RawAddress | Should Be 'foo.com:80' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be 'foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.2' + $endpoint.RawAddress | Should -Be 'foo.com:80' } It 'Set just an IPv4 address' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.1' -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 8080 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set just an IPv4 address for all' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address 'all' -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '0.0.0.0' - $endpoint.RawAddress | Should Be '0.0.0.0:8080' + $endpoint.Port | Should -Be 8080 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '0.0.0.0' + $endpoint.RawAddress | Should -Be '0.0.0.0:8080' } It 'Set just an IPv4 address with colon' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.1' -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 8080 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 8080 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set just a port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set just a port with colon' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set both IPv4 address and port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Set both IPv4 address and port for all' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '*' -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = ($PodeContext.Server.Endpoints.Values | Select-Object -First 1) - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.FriendlyName | Should Be 'localhost' - $endpoint.Address.ToString() | Should Be '0.0.0.0' - $endpoint.RawAddress | Should Be '0.0.0.0:80' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.FriendlyName | Should -Be 'localhost' + $endpoint.Address.ToString() | Should -Be '0.0.0.0' + $endpoint.RawAddress | Should -Be '0.0.0.0:80' } It 'Throws error for an invalid IPv4' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '256.0.0.1' -Protocol 'HTTP' } | Should Throw 'Invalid IP Address' + { Add-PodeEndpoint -Address '256.0.0.1' -Protocol 'HTTP' } | Should -Throw -ExpectedMessage '*Invalid IP Address*' - $PodeContext.Server.Types | Should Be $null - $PodeContext.Server.Endpoints.Count | Should Be 0 + $PodeContext.Server.Types | Should -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 0 } It 'Throws error for an invalid IPv4 address with port' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '256.0.0.1' -Port 80 -Protocol 'HTTP' } | Should Throw 'Invalid IP Address' + { Add-PodeEndpoint -Address '256.0.0.1' -Port 80 -Protocol 'HTTP' } | Should -Throw -ExpectedMessage '*Invalid IP Address*' - $PodeContext.Server.Types | Should Be $null - $PodeContext.Server.Endpoints.Count | Should Be 0 + $PodeContext.Server.Types | Should -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 0 } It 'Add two endpoints to listen on, of the same type' { @@ -239,19 +244,19 @@ Describe 'Add-PodeEndpoint' { $ep1 = (Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -PassThru) $ep2 = (Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -PassThru) - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 2 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints[$ep1.Name] - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints[$ep2.Name] - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be 'pode.foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be 'pode.foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, with different names' { @@ -259,21 +264,21 @@ Describe 'Add-PodeEndpoint' { Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Example1' Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example2' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 2 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Example1'] - $endpoint.Port | Should Be 80 - $endpoint.Name | Should Be 'Example1' - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.Name | Should -Be 'Example1' + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Example2'] - $endpoint.Port | Should Be 80 - $endpoint.Name | Should Be 'Example2' - $endpoint.HostName | Should Be 'pode.foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.Name | Should -Be 'Example2' + $endpoint.HostName | Should -Be 'pode.foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, one of HTTP and one of HTTPS' { @@ -281,19 +286,19 @@ Describe 'Add-PodeEndpoint' { Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Http' Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTPS' -Name 'Https' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 2 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Http'] - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Https'] - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be 'pode.foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be 'pode.foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, but one added as they are the same' { @@ -301,27 +306,27 @@ Describe 'Add-PodeEndpoint' { Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' - $PodeContext.Server.Types | Should Be 'HTTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 1 + $PodeContext.Server.Types | Should -Be 'HTTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 1 $endpoint = @($PodeContext.Server.Endpoints.Values)[0] - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Allows adding two endpoints of different types' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'SMTP' - $PodeContext.Server.Endpoints.Count | Should Be 2 + $PodeContext.Server.Endpoints.Count | Should -Be 2 } It 'Throws error when adding two endpoints with the same name' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'HTTP' -Name 'Example' - { Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example' } | Should Throw 'already been defined' + { Add-PodeEndpoint -Address 'pode.foo.com' -Port 80 -Protocol 'HTTP' -Name 'Example' } | Should -Throw -ExpectedMessage '*already been defined*' } It 'Add two endpoints to listen on, one of SMTP and one of SMTPS' { @@ -329,19 +334,19 @@ Describe 'Add-PodeEndpoint' { Add-PodeEndpoint -Address '127.0.0.1' -Port 25 -Protocol 'SMTP' -Name 'Smtp' Add-PodeEndpoint -Address 'pode.mail.com' -Port 465 -Protocol 'SMTPS' -Name 'Smtps' - $PodeContext.Server.Types | Should Be 'SMTP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 2 + $PodeContext.Server.Types | Should -Be 'SMTP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Smtp'] - $endpoint.Port | Should Be 25 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 25 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Smtps'] - $endpoint.Port | Should Be 465 - $endpoint.HostName | Should Be 'pode.mail.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 465 + $endpoint.HostName | Should -Be 'pode.mail.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Add two endpoints to listen on, one of TCP and one of TCPS' { @@ -349,37 +354,38 @@ Describe 'Add-PodeEndpoint' { Add-PodeEndpoint -Address '127.0.0.1' -Port 80 -Protocol 'TCP' -Name 'Tcp' Add-PodeEndpoint -Address 'pode.foo.com' -Port 443 -Protocol 'TCPS' -Name 'Tcps' - $PodeContext.Server.Types | Should Be 'TCP' - $PodeContext.Server.Endpoints | Should Not Be $null - $PodeContext.Server.Endpoints.Count | Should Be 2 + $PodeContext.Server.Types | Should -Be 'TCP' + $PodeContext.Server.Endpoints | Should -Not -Be $null + $PodeContext.Server.Endpoints.Count | Should -Be 2 $endpoint = $PodeContext.Server.Endpoints['Tcp'] - $endpoint.Port | Should Be 80 - $endpoint.HostName | Should Be '' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 80 + $endpoint.HostName | Should -Be '' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' $endpoint = $PodeContext.Server.Endpoints['Tcps'] - $endpoint.Port | Should Be 443 - $endpoint.HostName | Should Be 'pode.foo.com' - $endpoint.Address.ToString() | Should Be '127.0.0.1' + $endpoint.Port | Should -Be 443 + $endpoint.HostName | Should -Be 'pode.foo.com' + $endpoint.Address.ToString() | Should -Be '127.0.0.1' } It 'Throws an error for not running as admin' { Mock Test-PodeIsAdminUser { return $false } $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; 'Type' = $null } - { Add-PodeEndpoint -Address '127.0.0.2' -Protocol 'HTTP' } | Should Throw 'Must be running with admin' + { Add-PodeEndpoint -Address '127.0.0.2' -Protocol 'HTTP' } | Should -Throw -ExpectedMessage '*Must be running with admin*' } } } Describe 'Get-PodeEndpoint' { - Mock Test-PodeIPAddress { return $true } - Mock Test-PodeIsAdminUser { return $true } + BeforeAll { + Mock Test-PodeIPAddress { return $true } + Mock Test-PodeIsAdminUser { return $true } } It 'Returns no Endpoints' { $PodeContext.Server = @{ Endpoints = @{}; EndpointsMap = @{}; Type = $null } $endpoints = Get-PodeEndpoint - $endpoints.Length | Should Be 0 + $endpoints.Length | Should -Be 0 } It 'Returns all Endpoints' { @@ -390,7 +396,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint - $endpoints.Length | Should Be 3 + $endpoints.Length | Should -Be 3 } It 'Returns 3 endpoints by address - combination of ip/hostname' { @@ -401,7 +407,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address '127.0.0.1' - $endpoints.Length | Should Be 3 + $endpoints.Length | Should -Be 3 } It 'Returns 2 endpoints by hostname, and 3 by ip' { @@ -412,10 +418,10 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' - $endpoints.Length | Should Be 2 + $endpoints.Length | Should -Be 2 $endpoints = Get-PodeEndpoint -Address '127.0.0.1' - $endpoints.Length | Should Be 3 + $endpoints.Length | Should -Be 3 } It 'Returns 2 endpoints by hostname - old' { @@ -426,7 +432,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address 'pode.foo.com' - $endpoints.Length | Should Be 2 + $endpoints.Length | Should -Be 2 } It 'Returns 2 endpoints by port' { @@ -437,7 +443,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Port 80 - $endpoints.Length | Should Be 2 + $endpoints.Length | Should -Be 2 } It 'Returns all endpoints by protocol' { @@ -448,7 +454,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Protocol Http - $endpoints.Length | Should Be 3 + $endpoints.Length | Should -Be 3 } It 'Returns 2 endpoints by name' { @@ -459,7 +465,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' -Name 'Dev' $endpoints = Get-PodeEndpoint -Name Admin, User - $endpoints.Length | Should Be 2 + $endpoints.Length | Should -Be 2 } It 'Returns 1 endpoint using everything' { @@ -470,7 +476,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'pode.foo.com' -Port 8080 -Protocol 'HTTP' -Name 'Dev' $endpoints = Get-PodeEndpoint -Hostname 'pode.foo.com' -Port 80 -Protocol Http -Name User - $endpoints.Length | Should Be 1 + $endpoints.Length | Should -Be 1 } It 'Returns endpoint set using wildcard' { @@ -479,7 +485,7 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address '*' -Port 80 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address '*' - $endpoints.Length | Should Be 1 + $endpoints.Length | Should -Be 1 } It 'Returns endpoint set using localhost' { @@ -488,33 +494,34 @@ Describe 'Get-PodeEndpoint' { Add-PodeEndpoint -Address 'localhost' -Port 80 -Protocol 'HTTP' $endpoints = Get-PodeEndpoint -Address 'localhost' - $endpoints.Length | Should Be 1 + $endpoints.Length | Should -Be 1 } } Describe 'Import-PodeModule' { Context 'Invalid parameters supplied' { It 'Throw null path parameter error' { - { Import-PodeModule -Path $null } | Should Throw 'it is an empty string' + { Import-PodeModule -Path $null } | Should -Throw -ExpectedMessage '*it is an empty string*' } It 'Throw empty path parameter error' { - { Import-PodeModule -Path ([string]::Empty) } | Should Throw 'it is an empty string' + { Import-PodeModule -Path ([string]::Empty) } | Should -Throw -ExpectedMessage '*it is an empty string*' } It 'Throw null name parameter error' { - { Import-PodeModule -Name $null } | Should Throw 'it is an empty string' + { Import-PodeModule -Name $null } | Should -Throw -ExpectedMessage '*it is an empty string*' } It 'Throw empty name parameter error' { - { Import-PodeModule -Name ([string]::Empty) } | Should Throw 'it is an empty string' + { Import-PodeModule -Name ([string]::Empty) } | Should -Throw -ExpectedMessage '*it is an empty string*' } } Context 'Valid parameters supplied' { - Mock Resolve-Path { return @{ 'Path' = 'c:/some/file.txt' } } - Mock Import-Module { } - Mock Test-PodePath { return $true } + BeforeAll { + Mock Resolve-Path { return @{ 'Path' = 'c:/some/file.txt' } } + Mock Import-Module { } + Mock Test-PodePath { return $true } } It 'Returns null for no shared state in context' { Import-PodeModule -Path 'file.txt' @@ -530,113 +537,113 @@ Describe 'New-PodeAutoRestartServer' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 0 - $PodeContext.Schedules.Items.Count | Should Be 0 + $PodeContext.Timers.Items.Count | Should -Be 0 + $PodeContext.Schedules.Items.Count | Should -Be 0 } It 'Creates a timer for a period server restart' { Mock 'Get-PodeConfig' { return @{ - 'server' = @{ - 'restart'= @{ - 'period' = 180; + 'server' = @{ + 'restart' = @{ + 'period' = 180 + } } - } - } } + } } $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 1 - $PodeContext.Schedules.Items.Count | Should Be 0 - $PodeContext.Timers.Items.Keys[0] | Should Be '__pode_restart_period__' + $PodeContext.Timers.Items.Count | Should -Be 1 + $PodeContext.Schedules.Items.Count | Should -Be 0 + $PodeContext.Timers.Items.Keys[0] | Should -Be '__pode_restart_period__' } It 'Creates a schedule for a timed server restart' { Mock 'Get-PodeConfig' { return @{ - 'server' = @{ - 'restart'= @{ - 'times' = @('18:00'); + 'server' = @{ + 'restart' = @{ + 'times' = @('18:00') + } } - } - } } + } } $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 0 - $PodeContext.Schedules.Items.Count | Should Be 1 - $PodeContext.Schedules.Items.Keys[0] | Should Be '__pode_restart_times__' + $PodeContext.Timers.Items.Count | Should -Be 0 + $PodeContext.Schedules.Items.Count | Should -Be 1 + $PodeContext.Schedules.Items.Keys[0] | Should -Be '__pode_restart_times__' } It 'Creates a schedule for a cron server restart' { Mock 'Get-PodeConfig' { return @{ - 'server' = @{ - 'restart'= @{ - 'crons' = @('@minutely'); + 'server' = @{ + 'restart' = @{ + 'crons' = @('@minutely') + } } - } - } } + } } $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 0 - $PodeContext.Schedules.Items.Count | Should Be 1 - $PodeContext.Schedules.Items.Keys[0] | Should Be '__pode_restart_crons__' + $PodeContext.Timers.Items.Count | Should -Be 0 + $PodeContext.Schedules.Items.Count | Should -Be 1 + $PodeContext.Schedules.Items.Keys[0] | Should -Be '__pode_restart_crons__' } It 'Creates a timer and schedule for a period and cron server restart' { Mock 'Get-PodeConfig' { return @{ - 'server' = @{ - 'restart'= @{ - 'period' = 180; - 'crons' = @('@minutely'); + 'server' = @{ + 'restart' = @{ + 'period' = 180 + 'crons' = @('@minutely') + } } - } - } } + } } $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 1 - $PodeContext.Schedules.Items.Count | Should Be 1 - $PodeContext.Timers.Items.Keys[0] | Should Be '__pode_restart_period__' - $PodeContext.Schedules.Items.Keys[0] | Should Be '__pode_restart_crons__' + $PodeContext.Timers.Items.Count | Should -Be 1 + $PodeContext.Schedules.Items.Count | Should -Be 1 + $PodeContext.Timers.Items.Keys[0] | Should -Be '__pode_restart_period__' + $PodeContext.Schedules.Items.Keys[0] | Should -Be '__pode_restart_crons__' } It 'Creates a timer and schedule for a period and timed server restart' { Mock 'Get-PodeConfig' { return @{ - 'server' = @{ - 'restart'= @{ - 'period' = 180; - 'times' = @('18:00'); + 'server' = @{ + 'restart' = @{ + 'period' = 180 + 'times' = @('18:00') + } } - } - } } + } } $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 1 - $PodeContext.Schedules.Items.Count | Should Be 1 - $PodeContext.Timers.Items.Keys[0] | Should Be '__pode_restart_period__' - $PodeContext.Schedules.Items.Keys[0] | Should Be '__pode_restart_times__' + $PodeContext.Timers.Items.Count | Should -Be 1 + $PodeContext.Schedules.Items.Count | Should -Be 1 + $PodeContext.Timers.Items.Keys[0] | Should -Be '__pode_restart_period__' + $PodeContext.Schedules.Items.Keys[0] | Should -Be '__pode_restart_times__' } It 'Creates two schedules for a cron and timed server restart' { Mock 'Get-PodeConfig' { return @{ - 'server' = @{ - 'restart'= @{ - 'crons' = @('@minutely'); - 'times' = @('18:00'); + 'server' = @{ + 'restart' = @{ + 'crons' = @('@minutely') + 'times' = @('18:00') + } } - } - } } + } } $PodeContext = @{ 'Timers' = @{ Items = @{} }; 'Schedules' = @{ Items = @{} }; } New-PodeAutoRestartServer - $PodeContext.Timers.Items.Count | Should Be 0 - $PodeContext.Schedules.Items.Count | Should Be 2 + $PodeContext.Timers.Items.Count | Should -Be 0 + $PodeContext.Schedules.Items.Count | Should -Be 2 } } \ No newline at end of file diff --git a/tests/unit/Cookies.Tests.ps1 b/tests/unit/Cookies.Tests.ps1 index 8a657f006..f9a992fd2 100644 --- a/tests/unit/Cookies.Tests.ps1 +++ b/tests/unit/Cookies.Tests.ps1 @@ -1,25 +1,28 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Test-PodeCookie' { It 'Returns true' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } - Test-PodeCookie -Name 'test' | Should Be $true + Test-PodeCookie -Name 'test' | Should -Be $true } It 'Returns false for no value' { $WebEvent = @{ 'Cookies' = @{ } } - Test-PodeCookie -Name 'test' | Should Be $false + Test-PodeCookie -Name 'test' | Should -Be $false } It 'Returns false for not existing' { $WebEvent = @{ 'Cookies' = @{ } } - Test-PodeCookie -Name 'test' | Should Be $false + Test-PodeCookie -Name 'test' | Should -Be $false } } @@ -27,15 +30,15 @@ Describe 'Test-PodeCookieSigned' { It 'Returns false for no value' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ } - } + } } - Test-PodeCookieSigned -Name 'test' | Should Be $false + Test-PodeCookieSigned -Name 'test' | Should -Be $false } It 'Returns false for not existing' { $WebEvent = @{ 'Cookies' = @{} } - Test-PodeCookieSigned -Name 'test' | Should Be $false + Test-PodeCookieSigned -Name 'test' | Should -Be $false } It 'Throws error for no secret being passed' { @@ -43,10 +46,10 @@ Describe 'Test-PodeCookieSigned' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } - { Test-PodeCookieSigned -Name 'test' } | Should Throw 'argument is null' + { Test-PodeCookieSigned -Name 'test' } | Should -Throw -ExpectedMessage '*argument is null*' Assert-MockCalled Invoke-PodeValueUnsign -Times 0 -Scope It } @@ -55,10 +58,10 @@ Describe 'Test-PodeCookieSigned' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } - Test-PodeCookieSigned -Name 'test' -Secret 'key' | Should Be $false + Test-PodeCookieSigned -Name 'test' -Secret 'key' | Should -Be $false Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It } @@ -67,10 +70,10 @@ Describe 'Test-PodeCookieSigned' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } - Test-PodeCookieSigned -Name 'test' -Secret 'key' | Should Be $true + Test-PodeCookieSigned -Name 'test' -Secret 'key' | Should -Be $true Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It } @@ -81,16 +84,16 @@ Describe 'Test-PodeCookieSigned' { 'Cookies' = @{ 'Secrets' = @{ 'global' = 'key' } - } - } + } + } } $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } - Test-PodeCookieSigned -Name 'test' -Secret (Get-PodeCookieSecret -Global) | Should Be $true + Test-PodeCookieSigned -Name 'test' -Secret (Get-PodeCookieSecret -Global) | Should -Be $true Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It } } @@ -99,30 +102,30 @@ Describe 'Get-PodeCookie' { It 'Returns null for no value' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ } - } + } } - Get-PodeCookie -Name 'test' | Should Be $null + Get-PodeCookie -Name 'test' | Should -Be $null } It 'Returns null for not existing' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ } - } + } } - Get-PodeCookie -Name 'test' | Should Be $null + Get-PodeCookie -Name 'test' | Should -Be $null } It 'Returns a cookie, with no secret' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } $c = Get-PodeCookie -Name 'test' - $c | Should Not Be $null - $c.Value | Should Be 'example' + $c | Should -Not -Be $null + $c.Value | Should -Be 'example' } It 'Returns a cookie, with secret but not valid signed' { @@ -130,12 +133,12 @@ Describe 'Get-PodeCookie' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } $c = Get-PodeCookie -Name 'test' -Secret 'key' - $c | Should Not Be $null - $c.Value | Should Be 'example' + $c | Should -Not -Be $null + $c.Value | Should -Be 'example' Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It } @@ -145,12 +148,12 @@ Describe 'Get-PodeCookie' { $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } $c = Get-PodeCookie -Name 'test' -Secret 'key' - $c | Should Not Be $null - $c.Value | Should Be 'some-id' + $c | Should -Not -Be $null + $c.Value | Should -Be 'some-id' Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It } @@ -162,18 +165,18 @@ Describe 'Get-PodeCookie' { 'Cookies' = @{ 'Secrets' = @{ 'global' = 'key' } - } - } + } + } } $WebEvent = @{ 'Cookies' = @{ 'test' = @{ 'Value' = 'example' } - } + } } $c = Get-PodeCookie -Name 'test' -Secret (Get-PodeCookieSecret -Global) - $c | Should Not Be $null - $c.Value | Should Be 'some-id' + $c | Should -Not -Be $null + $c.Value | Should -Be 'some-id' Assert-MockCalled Invoke-PodeValueUnsign -Times 1 -Scope It } @@ -183,7 +186,7 @@ Describe 'Set-PodeCookie' { It 'Adds simple cookie to response' { $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} - }; 'PendingCookies' = @{} + }; 'PendingCookies' = @{} } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { @@ -192,21 +195,21 @@ Describe 'Set-PodeCookie' { } $c = Set-PodeCookie -Name 'test' -Value 'example' - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'example' - $c.Secure | Should Be $false - $c.Discard | Should Be $false - $c.HttpOnly | Should Be $false - $c.Expires | Should Be ([datetime]::MinValue) + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'example' + $c.Secure | Should -Be $false + $c.Discard | Should -Be $false + $c.HttpOnly | Should -Be $false + $c.Expires | Should -Be ([datetime]::MinValue) $c = $WebEvent.PendingCookies['test'] - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'example' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'example' $h = $WebEvent.Response.Headers['Set-Cookie'] - $h | Should Not Be $null + $h | Should -Not -Be $null } It 'Adds signed cookie to response' { @@ -214,7 +217,7 @@ Describe 'Set-PodeCookie' { $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} - }; 'PendingCookies' = @{} + }; 'PendingCookies' = @{} } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { @@ -223,17 +226,17 @@ Describe 'Set-PodeCookie' { } $c = Set-PodeCookie -Name 'test' -Value 'example' -Secret 'key' - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'some-id' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'some-id' $c = $WebEvent.PendingCookies['test'] - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'some-id' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'some-id' $h = $WebEvent.Response.Headers['Set-Cookie'] - $h | Should Not Be $null + $h | Should -Not -Be $null Assert-MockCalled Invoke-PodeValueSign -Times 1 -Scope It } @@ -245,13 +248,13 @@ Describe 'Set-PodeCookie' { 'Cookies' = @{ 'Secrets' = @{ 'global' = 'key' } - } - } + } + } } $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} - }; 'PendingCookies' = @{} + }; 'PendingCookies' = @{} } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { @@ -260,17 +263,17 @@ Describe 'Set-PodeCookie' { } $c = Set-PodeCookie -Name 'test' -Value 'example' -Secret (Get-PodeCookieSecret -Global) - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'some-id' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'some-id' $c = $WebEvent.PendingCookies['test'] - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'some-id' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'some-id' $h = $WebEvent.Response.Headers['Set-Cookie'] - $h | Should Not Be $null + $h | Should -Not -Be $null Assert-MockCalled Invoke-PodeValueSign -Times 1 -Scope It } @@ -278,7 +281,7 @@ Describe 'Set-PodeCookie' { It 'Adds cookie to response with options' { $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} - }; 'PendingCookies' = @{} + }; 'PendingCookies' = @{} } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { @@ -288,24 +291,24 @@ Describe 'Set-PodeCookie' { $c = Set-PodeCookie -Name 'test' -Value 'example' -HttpOnly -Secure -Discard - $c | Should Not Be $null - $c.Secure | Should Be $true - $c.Discard | Should Be $true - $c.HttpOnly | Should Be $true + $c | Should -Not -Be $null + $c.Secure | Should -Be $true + $c.Discard | Should -Be $true + $c.HttpOnly | Should -Be $true $c = $WebEvent.PendingCookies['test'] - $c.Secure | Should Be $true - $c.Discard | Should Be $true - $c.HttpOnly | Should Be $true + $c.Secure | Should -Be $true + $c.Discard | Should -Be $true + $c.HttpOnly | Should -Be $true $h = $WebEvent.Response.Headers['Set-Cookie'] - $h | Should Not Be $null + $h | Should -Not -Be $null } It 'Adds cookie to response with TTL' { $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} - }; 'PendingCookies' = @{} + }; 'PendingCookies' = @{} } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { @@ -314,24 +317,24 @@ Describe 'Set-PodeCookie' { } $c = Set-PodeCookie -Name 'test' -Value 'example' -Duration 3600 - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'example' - ($c.Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should Be $true + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'example' + ($c.Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should -Be $true $c = $WebEvent.PendingCookies['test'] - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'example' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'example' $h = $WebEvent.Response.Headers['Set-Cookie'] - $h | Should Not Be $null + $h | Should -Not -Be $null } It 'Adds cookie to response with Expiry' { $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} - }; 'PendingCookies' = @{} + }; 'PendingCookies' = @{} } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { @@ -340,18 +343,18 @@ Describe 'Set-PodeCookie' { } $c = Set-PodeCookie -Name 'test' -Value 'example' -ExpiryDate ([datetime]::UtcNow.AddDays(2)) - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'example' - ($c.Expires -gt [datetime]::UtcNow.AddDays(1)) | Should Be $true + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'example' + ($c.Expires -gt [datetime]::UtcNow.AddDays(1)) | Should -Be $true $c = $WebEvent.PendingCookies['test'] - $c | Should Not Be $null - $c.Name | Should Be 'test' - $c.Value | Should Be 'example' + $c | Should -Not -Be $null + $c.Name | Should -Be 'test' + $c.Value | Should -Be 'example' $h = $WebEvent.Response.Headers['Set-Cookie'] - $h | Should Not Be $null + $h | Should -Not -Be $null } } @@ -364,11 +367,11 @@ Describe 'Update-PodeCookieExpiry' { } 'PendingCookies' = @{ 'test' = @{ 'Name' = 'test'; 'Expires' = [datetime]::UtcNow } - } + } } Update-PodeCookieExpiry -Name 'test' -Duration 3600 - ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should Be $true + ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should -Be $true } It 'Updates the expiry based on TTL, using cookie from request' { @@ -379,11 +382,11 @@ Describe 'Update-PodeCookieExpiry' { $script:WebEvent = @{ 'Response' = @{ 'Headers' = @{} } - 'PendingCookies' = @{} + 'PendingCookies' = @{} } Update-PodeCookieExpiry -Name 'test' -Duration 3600 - ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should Be $true + ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddSeconds(3000)) | Should -Be $true } It 'Updates the expiry based on Expiry' { @@ -394,11 +397,11 @@ Describe 'Update-PodeCookieExpiry' { } 'PendingCookies' = @{ 'test' = @{ 'Name' = 'test'; 'Expires' = [datetime]::UtcNow } - } + } } Update-PodeCookieExpiry -Name 'test' -Expiry ([datetime]::UtcNow.AddDays(2)) - ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddDays(1)) | Should Be $true + ($WebEvent.PendingCookies['test'].Expires -gt [datetime]::UtcNow.AddDays(1)) | Should -Be $true } It 'Expiry remains unchanged on 0 TTL' { @@ -411,11 +414,11 @@ Describe 'Update-PodeCookieExpiry' { } 'PendingCookies' = @{ 'test' = @{ 'Name' = 'test'; 'Expires' = $ttl } - } + } } Update-PodeCookieExpiry -Name 'test' - $WebEvent.PendingCookies['test'].Expires | Should Be $ttl + $WebEvent.PendingCookies['test'].Expires | Should -Be $ttl } It 'Expiry remains unchanged on negative TTL' { @@ -428,11 +431,11 @@ Describe 'Update-PodeCookieExpiry' { } 'PendingCookies' = @{ 'test' = @{ 'Name' = 'test'; 'Expires' = $ttl } - } + } } Update-PodeCookieExpiry -Name 'test' -Duration -1 - $WebEvent.PendingCookies['test'].Expires | Should Be $ttl + $WebEvent.PendingCookies['test'].Expires | Should -Be $ttl } } @@ -445,13 +448,13 @@ Describe 'Remove-PodeCookie' { } 'PendingCookies' = @{ 'test' = @{ 'Name' = 'test'; 'Discard' = $false; 'Expires' = [datetime]::UtcNow } - } + } } Remove-PodeCookie -Name 'test' - $WebEvent.PendingCookies['test'].Discard | Should Be $true - ($WebEvent.PendingCookies['test'].Expires -lt [datetime]::UtcNow) | Should Be $true + $WebEvent.PendingCookies['test'].Discard | Should -Be $true + ($WebEvent.PendingCookies['test'].Expires -lt [datetime]::UtcNow) | Should -Be $true } It 'Flags the cookie for removal, using a cookie from the request' { @@ -462,38 +465,38 @@ Describe 'Remove-PodeCookie' { $WebEvent = @{ 'Response' = @{ 'Headers' = @{} } - 'PendingCookies' = @{} + 'PendingCookies' = @{} } Remove-PodeCookie -Name 'test' - $WebEvent.PendingCookies['test'].Discard | Should Be $true - ($WebEvent.PendingCookies['test'].Expires -lt [datetime]::UtcNow) | Should Be $true + $WebEvent.PendingCookies['test'].Discard | Should -Be $true + ($WebEvent.PendingCookies['test'].Expires -lt [datetime]::UtcNow) | Should -Be $true } } Describe 'Invoke-PodeValueSign' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Invoke-PodeValueSign -Value $null -Secret 'key' } | Should Throw 'argument is null or empty' + { Invoke-PodeValueSign -Value $null -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws empty value error' { - { Invoke-PodeValueSign -Value '' -Secret 'key' } | Should Throw 'argument is null or empty' + { Invoke-PodeValueSign -Value '' -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws null secret error' { - { Invoke-PodeValueSign -Value 'value' -Secret $null } | Should Throw 'argument is null or empty' + { Invoke-PodeValueSign -Value 'value' -Secret $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws empty secret error' { - { Invoke-PodeValueSign -Value 'value' -Secret '' } | Should Throw 'argument is null or empty' + { Invoke-PodeValueSign -Value 'value' -Secret '' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } } Context 'Valid parameters' { It 'Returns signed encrypted data' { - Invoke-PodeValueSign -Value 'value' -Secret 'key' | Should Be 's:value.kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=' + Invoke-PodeValueSign -Value 'value' -Secret 'key' | Should -Be 's:value.kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=' } } } @@ -501,37 +504,37 @@ Describe 'Invoke-PodeValueSign' { Describe 'Invoke-PodeValueUnsign' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Invoke-PodeValueUnsign -Value $null -Secret 'key' } | Should Throw 'argument is null or empty' + { Invoke-PodeValueUnsign -Value $null -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws empty value error' { - { Invoke-PodeValueUnsign -Value '' -Secret 'key' } | Should Throw 'argument is null or empty' + { Invoke-PodeValueUnsign -Value '' -Secret 'key' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws null secret error' { - { Invoke-PodeValueUnsign -Value 'value' -Secret $null } | Should Throw 'argument is null or empty' + { Invoke-PodeValueUnsign -Value 'value' -Secret $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws empty secret error' { - { Invoke-PodeValueUnsign -Value 'value' -Secret '' } | Should Throw 'argument is null or empty' + { Invoke-PodeValueUnsign -Value 'value' -Secret '' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } } Context 'Valid parameters' { It 'Returns signed encrypted data' { - Invoke-PodeValueUnsign -Value 's:value.kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=' -Secret 'key' | Should Be 'value' + Invoke-PodeValueUnsign -Value 's:value.kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=' -Secret 'key' | Should -Be 'value' } It 'Returns null for unsign data with no tag' { - Invoke-PodeValueUnsign -Value 'value' -Secret 'key' | Should Be $null + Invoke-PodeValueUnsign -Value 'value' -Secret 'key' | Should -Be $null } It 'Returns null for unsign data with no period' { - Invoke-PodeValueUnsign -Value 's:value' -Secret 'key' | Should Be $null + Invoke-PodeValueUnsign -Value 's:value' -Secret 'key' | Should -Be $null } It 'Returns null for invalid signing' { - Invoke-PodeValueUnsign -Value 's:value.random' -Secret 'key' | Should Be $null + Invoke-PodeValueUnsign -Value 's:value.random' -Secret 'key' | Should -Be $null } } } @@ -539,7 +542,7 @@ Describe 'Invoke-PodeValueUnsign' { Describe 'ConvertTo-PodeCookie' { It 'Returns empty for no cookie' { $r = ConvertTo-PodeCookie -Cookie $null - $r.Count | Should Be 0 + $r.Count | Should -Be 0 } It 'Returns a mapped cookie' { @@ -548,60 +551,60 @@ Describe 'ConvertTo-PodeCookie' { $r = ConvertTo-PodeCookie -Cookie $c - $r.Count | Should Be 10 - $r.Name | Should Be 'date' - $r.Value | Should Be $now - $r.Signed | Should Be $false - $r.HttpOnly | Should Be $false - $r.Discard | Should Be $false - $r.Secure | Should Be $false + $r.Count | Should -Be 10 + $r.Name | Should -Be 'date' + $r.Value | Should -Be $now + $r.Signed | Should -Be $false + $r.HttpOnly | Should -Be $false + $r.Discard | Should -Be $false + $r.Secure | Should -Be $false } } Describe 'ConvertTo-PodeCookieString' { It 'Returns name, value' { $c = [System.Net.Cookie]::new('name', 'value') - ConvertTo-PodeCookieString -Cookie $c | Should Be 'name=value' + ConvertTo-PodeCookieString -Cookie $c | Should -Be 'name=value' } It 'Returns name, value, discard' { $c = [System.Net.Cookie]::new('name', 'value') $c.Discard = $true - ConvertTo-PodeCookieString -Cookie $c | Should Be 'name=value; Discard' + ConvertTo-PodeCookieString -Cookie $c | Should -Be 'name=value; Discard' } It 'Returns name, value, httponly' { $c = [System.Net.Cookie]::new('name', 'value') $c.HttpOnly = $true - ConvertTo-PodeCookieString -Cookie $c | Should Be 'name=value; HttpOnly' + ConvertTo-PodeCookieString -Cookie $c | Should -Be 'name=value; HttpOnly' } It 'Returns name, value, secure' { $c = [System.Net.Cookie]::new('name', 'value') $c.Secure = $true - ConvertTo-PodeCookieString -Cookie $c | Should Be 'name=value; Secure' + ConvertTo-PodeCookieString -Cookie $c | Should -Be 'name=value; Secure' } It 'Returns name, value, domain' { $c = [System.Net.Cookie]::new('name', 'value') $c.Domain = 'random.domain.name' - ConvertTo-PodeCookieString -Cookie $c | Should Be 'name=value; Domain=random.domain.name' + ConvertTo-PodeCookieString -Cookie $c | Should -Be 'name=value; Domain=random.domain.name' } It 'Returns name, value, path' { $c = [System.Net.Cookie]::new('name', 'value') $c.Path = '/api' - ConvertTo-PodeCookieString -Cookie $c | Should Be 'name=value; Path=/api' + ConvertTo-PodeCookieString -Cookie $c | Should -Be 'name=value; Path=/api' } It 'Returns name, value, max-age' { $c = [System.Net.Cookie]::new('name', 'value') $c.Expires = [datetime]::Now.AddDays(1) - ConvertTo-PodeCookieString -Cookie $c | Should Match 'name=value; Max-Age=\d+' + ConvertTo-PodeCookieString -Cookie $c | Should -Match 'name=value; Max-Age=\d+' } It 'Returns null for no name or value' { $c = @{ 'Name' = ''; 'Value' = '' } - ConvertTo-PodeCookieString -Cookie $c | Should Be $null + ConvertTo-PodeCookieString -Cookie $c | Should -Be $null } } \ No newline at end of file diff --git a/tests/unit/CronParser.Tests.ps1 b/tests/unit/CronParser.Tests.ps1 index 2ffb682ce..b5d7454e4 100644 --- a/tests/unit/CronParser.Tests.ps1 +++ b/tests/unit/CronParser.Tests.ps1 @@ -1,10 +1,14 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Get-PodeCronFields' { It 'Returns valid cron fields' { - Get-PodeCronFields | Should Be @( + Get-PodeCronFields | Should -Be @( 'Minute', 'Hour', 'DayOfMonth', @@ -17,9 +21,9 @@ Describe 'Get-PodeCronFields' { Describe 'Get-PodeCronFieldConstraints' { It 'Returns valid cron field constraints' { $constraints = Get-PodeCronFieldConstraints - $constraints | Should Not Be $null + $constraints | Should -Not -Be $null - $constraints.MinMax | Should Be @( + $constraints.MinMax | Should -Be @( @(0, 59), @(0, 23), @(1, 31), @@ -27,11 +31,11 @@ Describe 'Get-PodeCronFieldConstraints' { @(0, 6) ) - $constraints.DaysInMonths | Should Be @( + $constraints.DaysInMonths | Should -Be @( 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ) - $constraints.Months | Should Be @( + $constraints.Months | Should -Be @( 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ) @@ -41,263 +45,266 @@ Describe 'Get-PodeCronFieldConstraints' { Describe 'Get-PodeCronPredefined' { It 'Returns valid predefined values' { $values = Get-PodeCronPredefined - $values | Should Not Be $null - - $values['@minutely'] | Should Be '* * * * *' - $values['@hourly'] | Should Be '0 * * * *' - $values['@daily'] | Should Be '0 0 * * *' - $values['@weekly'] | Should Be '0 0 * * 0' - $values['@monthly'] | Should Be '0 0 1 * *' - $values['@quarterly'] | Should Be '0 0 1 1,4,7,10 *' - $values['@yearly'] | Should Be '0 0 1 1 *' - $values['@annually'] | Should Be '0 0 1 1 *' - $values['@twice-hourly'] | Should Be '0,30 * * * *' - $values['@twice-daily'] | Should Be '0 0,12 * * *' - $values['@twice-weekly'] | Should Be '0 0 * * 0,4' - $values['@twice-monthly'] | Should Be '0 0 1,15 * *' - $values['@twice-yearly'] | Should Be '0 0 1 1,6 *' - $values['@twice-annually'] | Should Be '0 0 1 1,6 *' + $values | Should -Not -Be $null + + $values['@minutely'] | Should -Be '* * * * *' + $values['@hourly'] | Should -Be '0 * * * *' + $values['@daily'] | Should -Be '0 0 * * *' + $values['@weekly'] | Should -Be '0 0 * * 0' + $values['@monthly'] | Should -Be '0 0 1 * *' + $values['@quarterly'] | Should -Be '0 0 1 1,4,7,10 *' + $values['@yearly'] | Should -Be '0 0 1 1 *' + $values['@annually'] | Should -Be '0 0 1 1 *' + $values['@twice-hourly'] | Should -Be '0,30 * * * *' + $values['@twice-daily'] | Should -Be '0 0,12 * * *' + $values['@twice-weekly'] | Should -Be '0 0 * * 0,4' + $values['@twice-monthly'] | Should -Be '0 0 1,15 * *' + $values['@twice-yearly'] | Should -Be '0 0 1 1,6 *' + $values['@twice-annually'] | Should -Be '0 0 1 1,6 *' } } Describe 'Get-PodeCronFieldAliases' { It 'Returns valid aliases' { $aliases = Get-PodeCronFieldAliases - $aliases | Should Not Be $null - - $aliases.Month.Jan | Should Be 1 - $aliases.Month.Feb | Should Be 2 - $aliases.Month.Mar | Should Be 3 - $aliases.Month.Apr | Should Be 4 - $aliases.Month.May | Should Be 5 - $aliases.Month.Jun | Should Be 6 - $aliases.Month.Jul | Should Be 7 - $aliases.Month.Aug | Should Be 8 - $aliases.Month.Sep | Should Be 9 - $aliases.Month.Oct | Should Be 10 - $aliases.Month.Nov | Should Be 11 - $aliases.Month.Dec | Should Be 12 - - $aliases.DayOfWeek.Sun | Should Be 0 - $aliases.DayOfWeek.Mon | Should Be 1 - $aliases.DayOfWeek.Tue | Should Be 2 - $aliases.DayOfWeek.Wed | Should Be 3 - $aliases.DayOfWeek.Thu | Should Be 4 - $aliases.DayOfWeek.Fri | Should Be 5 - $aliases.DayOfWeek.Sat | Should Be 6 + $aliases | Should -Not -Be $null + + $aliases.Month.Jan | Should -Be 1 + $aliases.Month.Feb | Should -Be 2 + $aliases.Month.Mar | Should -Be 3 + $aliases.Month.Apr | Should -Be 4 + $aliases.Month.May | Should -Be 5 + $aliases.Month.Jun | Should -Be 6 + $aliases.Month.Jul | Should -Be 7 + $aliases.Month.Aug | Should -Be 8 + $aliases.Month.Sep | Should -Be 9 + $aliases.Month.Oct | Should -Be 10 + $aliases.Month.Nov | Should -Be 11 + $aliases.Month.Dec | Should -Be 12 + + $aliases.DayOfWeek.Sun | Should -Be 0 + $aliases.DayOfWeek.Mon | Should -Be 1 + $aliases.DayOfWeek.Tue | Should -Be 2 + $aliases.DayOfWeek.Wed | Should -Be 3 + $aliases.DayOfWeek.Thu | Should -Be 4 + $aliases.DayOfWeek.Fri | Should -Be 5 + $aliases.DayOfWeek.Sat | Should -Be 6 } } Describe 'ConvertFrom-PodeCronExpression' { Context 'Invalid parameters supplied' { It 'Throw null expression parameter error' { - { ConvertFrom-PodeCronExpression -Expression $null } | Should Throw 'The argument is null or empty' + { ConvertFrom-PodeCronExpression -Expression $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } It 'Throw empty expression parameter error' { - { ConvertFrom-PodeCronExpression -Expression ([string]::Empty) } | Should Throw 'The argument is null or empty' + { ConvertFrom-PodeCronExpression -Expression ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } } Context 'Valid schedule parameters' { It 'Throws error for too few number of cron atoms' { - { ConvertFrom-PodeCronExpression -Expression '* * *' } | Should Throw 'Cron expression should only consist of 5 parts' + { ConvertFrom-PodeCronExpression -Expression '* * *' } | Should -Throw -ExpectedMessage '*Cron expression should only consist of 5 parts*' } It 'Throws error for too many number of cron atoms' { - { ConvertFrom-PodeCronExpression -Expression '* * * * * *' } | Should Throw 'Cron expression should only consist of 5 parts' + { ConvertFrom-PodeCronExpression -Expression '* * * * * *' } | Should -Throw -ExpectedMessage '*Cron expression should only consist of 5 parts*' } It 'Throws error for range atom with min>max' { - { ConvertFrom-PodeCronExpression -Expression '* * 20-15 * *' } | Should Throw 'should not be greater than the max value' + { ConvertFrom-PodeCronExpression -Expression '* * 20-15 * *' } | Should -Throw -ExpectedMessage '*should not be greater than the max value*' } It 'Throws error for range atom with invalid min' { - { ConvertFrom-PodeCronExpression -Expression '* * 0-5 * *' } | Should Throw 'is invalid, should be greater than/equal to' + { ConvertFrom-PodeCronExpression -Expression '* * 0-5 * *' } | Should -Throw -ExpectedMessage '*is invalid, should be greater than/equal to*' } It 'Throws error for range atom with invalid max' { - { ConvertFrom-PodeCronExpression -Expression '* * 1-32 * *' } | Should Throw 'is invalid, should be less than/equal to' + { ConvertFrom-PodeCronExpression -Expression '* * 1-32 * *' } | Should -Throw -ExpectedMessage '*is invalid, should be less than/equal to*' } It 'Throws error for atom with invalid min' { - { ConvertFrom-PodeCronExpression -Expression '* * 0 * *' } | Should Throw 'invalid, should be between' + { ConvertFrom-PodeCronExpression -Expression '* * 0 * *' } | Should -Throw -ExpectedMessage '*invalid, should be between*' } It 'Throws error for atom with invalid max' { - { ConvertFrom-PodeCronExpression -Expression '* * 32 * *' } | Should Throw 'invalid, should be between' + { ConvertFrom-PodeCronExpression -Expression '* * 32 * *' } | Should -Throw -ExpectedMessage '*invalid, should be between*' } + It 'Returns a valid cron object for predefined' { $cron = ConvertFrom-PodeCronExpression -Expression '@minutely' - $cron.Month.Values | Should Be $null - $cron.Month.Range.Min | Should Be 1 - $cron.Month.Range.Max | Should Be 12 - $cron.Month.Constraints[0] | Should Be 1 - $cron.Month.Constraints[1] | Should Be 12 - $cron.Month.Random | Should Be $false - - $cron.DayOfWeek.Values | Should Be $null - $cron.DayOfWeek.Range.Min | Should Be 0 - $cron.DayOfWeek.Range.Max | Should Be 6 - $cron.DayOfWeek.Constraints[0] | Should Be 0 - $cron.DayOfWeek.Constraints[1] | Should Be 6 - $cron.DayOfWeek.Random | Should Be $false - - $cron.Minute.Values | Should Be $null - $cron.Minute.Range.Min | Should Be 0 - $cron.Minute.Range.Max | Should Be 59 - $cron.Minute.Constraints[0] | Should Be 0 - $cron.Minute.Constraints[1] | Should Be 59 - $cron.Minute.Random | Should Be $false - - $cron.Hour.Values | Should Be $null - $cron.Hour.Range.Min | Should Be 0 - $cron.Hour.Range.Max | Should Be 23 - $cron.Hour.Constraints[0] | Should Be 0 - $cron.Hour.Constraints[1] | Should Be 23 - $cron.Hour.Random | Should Be $false - - $cron.Random | Should Be $false - - $cron.DayOfMonth.Values | Should Be $null - $cron.DayOfMonth.Range.Min | Should Be 1 - $cron.DayOfMonth.Range.Max | Should Be 31 - $cron.DayOfMonth.Constraints[0] | Should Be 1 - $cron.DayOfMonth.Constraints[1] | Should Be 31 - $cron.DayOfMonth.Random | Should Be $false + $cron.Month.Values | Should -Be $null + $cron.Month.Range.Min | Should -Be 1 + $cron.Month.Range.Max | Should -Be 12 + $cron.Month.Constraints[0] | Should -Be 1 + $cron.Month.Constraints[1] | Should -Be 12 + $cron.Month.Random | Should -Be $false + + $cron.DayOfWeek.Values | Should -Be $null + $cron.DayOfWeek.Range.Min | Should -Be 0 + $cron.DayOfWeek.Range.Max | Should -Be 6 + $cron.DayOfWeek.Constraints[0] | Should -Be 0 + $cron.DayOfWeek.Constraints[1] | Should -Be 6 + $cron.DayOfWeek.Random | Should -Be $false + + $cron.Minute.Values | Should -Be $null + $cron.Minute.Range.Min | Should -Be 0 + $cron.Minute.Range.Max | Should -Be 59 + $cron.Minute.Constraints[0] | Should -Be 0 + $cron.Minute.Constraints[1] | Should -Be 59 + $cron.Minute.Random | Should -Be $false + + $cron.Hour.Values | Should -Be $null + $cron.Hour.Range.Min | Should -Be 0 + $cron.Hour.Range.Max | Should -Be 23 + $cron.Hour.Constraints[0] | Should -Be 0 + $cron.Hour.Constraints[1] | Should -Be 23 + $cron.Hour.Random | Should -Be $false + + $cron.Random | Should -Be $false + + $cron.DayOfMonth.Values | Should -Be $null + $cron.DayOfMonth.Range.Min | Should -Be 1 + $cron.DayOfMonth.Range.Max | Should -Be 31 + $cron.DayOfMonth.Constraints[0] | Should -Be 1 + $cron.DayOfMonth.Constraints[1] | Should -Be 31 + $cron.DayOfMonth.Random | Should -Be $false } It 'Returns a valid cron object for expression' { $cron = ConvertFrom-PodeCronExpression -Expression '0/10 * * * 2' - $cron.Month.Values | Should Be $null - $cron.Month.Range.Min | Should Be 1 - $cron.Month.Range.Max | Should Be 12 - $cron.Month.Constraints[0] | Should Be 1 - $cron.Month.Constraints[1] | Should Be 12 - $cron.Month.Random | Should Be $false - - $cron.DayOfWeek.Values | Should Be 2 - $cron.DayOfWeek.Range.Min | Should Be $null - $cron.DayOfWeek.Range.Max | Should Be $null - $cron.DayOfWeek.Constraints[0] | Should Be 0 - $cron.DayOfWeek.Constraints[1] | Should Be 6 - $cron.DayOfWeek.Random | Should Be $false - - $cron.Minute.Values | Should Be @(0, 10, 20, 30, 40, 50) - $cron.Minute.Range.Min | Should Be $null - $cron.Minute.Range.Max | Should Be $null - $cron.Minute.Constraints[0] | Should Be 0 - $cron.Minute.Constraints[1] | Should Be 59 - $cron.Minute.Random | Should Be $false - - $cron.Hour.Values | Should Be $null - $cron.Hour.Range.Min | Should Be 0 - $cron.Hour.Range.Max | Should Be 23 - $cron.Hour.Constraints[0] | Should Be 0 - $cron.Hour.Constraints[1] | Should Be 23 - $cron.Hour.Random | Should Be $false - - $cron.Random | Should Be $false - - $cron.DayOfMonth.Values | Should Be $null - $cron.DayOfMonth.Range.Min | Should Be 1 - $cron.DayOfMonth.Range.Max | Should Be 31 - $cron.DayOfMonth.Constraints[0] | Should Be 1 - $cron.DayOfMonth.Constraints[1] | Should Be 31 - $cron.DayOfMonth.Random | Should Be $false + $cron.Month.Values | Should -Be $null + $cron.Month.Range.Min | Should -Be 1 + $cron.Month.Range.Max | Should -Be 12 + $cron.Month.Constraints[0] | Should -Be 1 + $cron.Month.Constraints[1] | Should -Be 12 + $cron.Month.Random | Should -Be $false + + $cron.DayOfWeek.Values | Should -Be 2 + $cron.DayOfWeek.Range.Min | Should -Be $null + $cron.DayOfWeek.Range.Max | Should -Be $null + $cron.DayOfWeek.Constraints[0] | Should -Be 0 + $cron.DayOfWeek.Constraints[1] | Should -Be 6 + $cron.DayOfWeek.Random | Should -Be $false + + $cron.Minute.Values | Should -Be @(0, 10, 20, 30, 40, 50) + $cron.Minute.Range.Min | Should -Be $null + $cron.Minute.Range.Max | Should -Be $null + $cron.Minute.Constraints[0] | Should -Be 0 + $cron.Minute.Constraints[1] | Should -Be 59 + $cron.Minute.Random | Should -Be $false + + $cron.Hour.Values | Should -Be $null + $cron.Hour.Range.Min | Should -Be 0 + $cron.Hour.Range.Max | Should -Be 23 + $cron.Hour.Constraints[0] | Should -Be 0 + $cron.Hour.Constraints[1] | Should -Be 23 + $cron.Hour.Random | Should -Be $false + + $cron.Random | Should -Be $false + + $cron.DayOfMonth.Values | Should -Be $null + $cron.DayOfMonth.Range.Min | Should -Be 1 + $cron.DayOfMonth.Range.Max | Should -Be 31 + $cron.DayOfMonth.Constraints[0] | Should -Be 1 + $cron.DayOfMonth.Constraints[1] | Should -Be 31 + $cron.DayOfMonth.Random | Should -Be $false } It 'Returns a valid cron object for expression using wildcard' { $cron = ConvertFrom-PodeCronExpression -Expression '*/10 * * * 2' - $cron.Month.Values | Should Be $null - $cron.Month.Range.Min | Should Be 1 - $cron.Month.Range.Max | Should Be 12 - $cron.Month.Constraints[0] | Should Be 1 - $cron.Month.Constraints[1] | Should Be 12 - $cron.Month.Random | Should Be $false - - $cron.DayOfWeek.Values | Should Be 2 - $cron.DayOfWeek.Range.Min | Should Be $null - $cron.DayOfWeek.Range.Max | Should Be $null - $cron.DayOfWeek.Constraints[0] | Should Be 0 - $cron.DayOfWeek.Constraints[1] | Should Be 6 - $cron.DayOfWeek.Random | Should Be $false - - $cron.Minute.Values | Should Be @(0, 10, 20, 30, 40, 50) - $cron.Minute.Range.Min | Should Be $null - $cron.Minute.Range.Max | Should Be $null - $cron.Minute.Constraints[0] | Should Be 0 - $cron.Minute.Constraints[1] | Should Be 59 - $cron.Minute.Random | Should Be $false - - $cron.Hour.Values | Should Be $null - $cron.Hour.Range.Min | Should Be 0 - $cron.Hour.Range.Max | Should Be 23 - $cron.Hour.Constraints[0] | Should Be 0 - $cron.Hour.Constraints[1] | Should Be 23 - $cron.Hour.Random | Should Be $false - - $cron.Random | Should Be $false - - $cron.DayOfMonth.Values | Should Be $null - $cron.DayOfMonth.Range.Min | Should Be 1 - $cron.DayOfMonth.Range.Max | Should Be 31 - $cron.DayOfMonth.Constraints[0] | Should Be 1 - $cron.DayOfMonth.Constraints[1] | Should Be 31 - $cron.DayOfMonth.Random | Should Be $false + $cron.Month.Values | Should -Be $null + $cron.Month.Range.Min | Should -Be 1 + $cron.Month.Range.Max | Should -Be 12 + $cron.Month.Constraints[0] | Should -Be 1 + $cron.Month.Constraints[1] | Should -Be 12 + $cron.Month.Random | Should -Be $false + + $cron.DayOfWeek.Values | Should -Be 2 + $cron.DayOfWeek.Range.Min | Should -Be $null + $cron.DayOfWeek.Range.Max | Should -Be $null + $cron.DayOfWeek.Constraints[0] | Should -Be 0 + $cron.DayOfWeek.Constraints[1] | Should -Be 6 + $cron.DayOfWeek.Random | Should -Be $false + + $cron.Minute.Values | Should -Be @(0, 10, 20, 30, 40, 50) + $cron.Minute.Range.Min | Should -Be $null + $cron.Minute.Range.Max | Should -Be $null + $cron.Minute.Constraints[0] | Should -Be 0 + $cron.Minute.Constraints[1] | Should -Be 59 + $cron.Minute.Random | Should -Be $false + + $cron.Hour.Values | Should -Be $null + $cron.Hour.Range.Min | Should -Be 0 + $cron.Hour.Range.Max | Should -Be 23 + $cron.Hour.Constraints[0] | Should -Be 0 + $cron.Hour.Constraints[1] | Should -Be 23 + $cron.Hour.Random | Should -Be $false + + $cron.Random | Should -Be $false + + $cron.DayOfMonth.Values | Should -Be $null + $cron.DayOfMonth.Range.Min | Should -Be 1 + $cron.DayOfMonth.Range.Max | Should -Be 31 + $cron.DayOfMonth.Constraints[0] | Should -Be 1 + $cron.DayOfMonth.Constraints[1] | Should -Be 31 + $cron.DayOfMonth.Random | Should -Be $false } } } -Describe 'Test-PodeCronExpression'{ - $inputDate = [datetime]::parseexact('2019-02-05 14:30', 'yyyy-MM-dd HH:mm', $null) +Describe 'Test-PodeCronExpression' { + BeforeAll { + $inputDate = [datetime]::parseexact('2019-02-05 14:30', 'yyyy-MM-dd HH:mm', $null) + } Context 'Passing test with fix cron' { It 'Returns true for a Tuesdays' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * * 2' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for Feb' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * 2 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 5th day of month' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 5 * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 5th day of Feb' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 5 2 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 14th hour' { $cron = ConvertFrom-PodeCronExpression -Expression '* 14 * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 30th minute' { $cron = ConvertFrom-PodeCronExpression -Expression '30 * * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for all values set' { $cron = ConvertFrom-PodeCronExpression -Expression '30 14 5 2 2' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } } @@ -306,31 +313,31 @@ Describe 'Test-PodeCronExpression'{ It 'Returns false for Jan' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * 1 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 4th day of Jan' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 4 1 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 13th hour' { $cron = ConvertFrom-PodeCronExpression -Expression '* 13 * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 20th minute' { $cron = ConvertFrom-PodeCronExpression -Expression '20 * * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for all values set' { $cron = ConvertFrom-PodeCronExpression -Expression '20 13 4 1 3' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } } @@ -338,43 +345,43 @@ Describe 'Test-PodeCronExpression'{ It 'Returns true for a Mondays and Tuesdays' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * * 1,2' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for Feb and Mar' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * 2,3 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 5th day of month and the 7th' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 5,7 * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 5th day of Feb and the 7th day of Mar' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 5,7 2,3 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 14th hour and the 16th hour' { $cron = ConvertFrom-PodeCronExpression -Expression '* 14,16 * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 30th minute and the 40th' { $cron = ConvertFrom-PodeCronExpression -Expression '30,40 * * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for all values set' { $cron = ConvertFrom-PodeCronExpression -Expression '30,40 14,16 5,7 2,3 1,2' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } } @@ -382,31 +389,31 @@ Describe 'Test-PodeCronExpression'{ It 'Returns false for Jan and Mar' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * 1,3 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 4th day of Jan and 5th day of Mar' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 4,5 1,3 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 13th hour and 15th' { $cron = ConvertFrom-PodeCronExpression -Expression '* 13,15 * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 20th minute and 29th' { $cron = ConvertFrom-PodeCronExpression -Expression '20,29 * * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for all values set' { $cron = ConvertFrom-PodeCronExpression -Expression '20,29 13,15 4,5 1,3 3,4' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } } @@ -414,43 +421,43 @@ Describe 'Test-PodeCronExpression'{ It 'Returns true for a Mondays to Tuesdays' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * * 1-2' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for Feb to Mar' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * 2-3 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 5th day of month to the 7th' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 5-7 * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 5th day of Feb to the 7th day of Mar' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 5-7 2-3 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 14th hour to the 16th hour' { $cron = ConvertFrom-PodeCronExpression -Expression '* 14-16 * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for 30th minute to the 40th' { $cron = ConvertFrom-PodeCronExpression -Expression '30-40 * * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } It 'Returns true for all values set' { $cron = ConvertFrom-PodeCronExpression -Expression '30-40 14-16 5-7 2-3 1-2' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $true + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $true } } @@ -458,143 +465,151 @@ Describe 'Test-PodeCronExpression'{ It 'Returns false for Mar to Dec' { $cron = ConvertFrom-PodeCronExpression -Expression '* * * 3-12 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 3rd day of Mar to 4th day of Dec' { $cron = ConvertFrom-PodeCronExpression -Expression '* * 3-4 3-12 *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 13th hour to 23rd' { $cron = ConvertFrom-PodeCronExpression -Expression '* 15-23 * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for 20th minute to 29th' { $cron = ConvertFrom-PodeCronExpression -Expression '20-29 * * * *' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } It 'Returns false for all values set' { $cron = ConvertFrom-PodeCronExpression -Expression '20-29 15-23 3-4 3-12 3-4' - Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should Be $false + Test-PodeCronExpression -Expression $cron -DateTime $inputDate | Should -Be $false } } } Describe 'Get-PodeCronNextTrigger' { - $inputDate = [datetime]::new(2020, 1, 1) - - It 'Returns the next minute' { - $exp = '* * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 1, 0, 1, 0)) - } + Describe 'InputDate 2020-01-01' { + BeforeEach { + $inputDate = [datetime]::new(2020, 1, 1) + } + It 'Returns the next minute' { + $exp = '* * * * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 1, 0, 1, 0)) + } - It 'Returns the next hour' { - $exp = '0 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 1, 1, 0, 0)) - } + It 'Returns the next hour' { + $exp = '0 * * * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 1, 1, 0, 0)) + } - It 'Returns the next day' { - $exp = '0 0 * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 2, 0, 0, 0)) - } + It 'Returns the next day' { + $exp = '0 0 * * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 2, 0, 0, 0)) + } - It 'Returns the next month' { - $exp = '0 0 1 * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 2, 1, 0, 0, 0)) - } + It 'Returns the next month' { + $exp = '0 0 1 * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 2, 1, 0, 0, 0)) + } - It 'Returns the next year' { - $exp = '0 0 1 1 *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2021, 1, 1, 0, 0, 0)) - } + It 'Returns the next year' { + $exp = '0 0 1 1 *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2021, 1, 1, 0, 0, 0)) + } - It 'Returns the friday 3rd' { - $exp = '0 0 * 1 FRI' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 3, 0, 0, 0)) - } + It 'Returns the friday 3rd' { + $exp = '0 0 * 1 FRI' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 3, 0, 0, 0)) + } - It 'Returns the 2023 friday' { - $exp = '0 0 13 1 FRI' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2023, 1, 13, 0, 0, 0)) - } + It 'Returns the 2023 friday' { + $exp = '0 0 13 1 FRI' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2023, 1, 13, 0, 0, 0)) + } - It 'Returns the null for after end time' { - $exp = '0 0 20 1 FRI' - $end = [datetime]::new(2020, 1, 19) - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate -EndTime $end | Should Be $null + It 'Returns the null for after end time' { + $exp = '0 0 20 1 FRI' + $end = [datetime]::new(2020, 1, 19) + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate -EndTime $end | Should -Be $null + } } - $inputDate = [datetime]::new(2020, 1, 15, 2, 30, 0) - - It 'Returns the minute but next hour' { - $exp = '20 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 15, 3, 20, 0)) - } + Describe 'InputDate 2020-01-15 02:30:00' { + BeforeEach { + $inputDate = [datetime]::new(2020, 1, 15, 2, 30, 0) + } + It 'Returns the minute but next hour' { + $exp = '20 * * * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 15, 3, 20, 0)) + } - It 'Returns the later minute but same hour' { - $exp = '20,40 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 15, 2, 40, 0)) - } + It 'Returns the later minute but same hour' { + $exp = '20,40 * * * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 15, 2, 40, 0)) + } - It 'Returns the next minute but same hour' { - $exp = '20-40 * * * *' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 1, 15, 2, 31, 0)) - } + It 'Returns the next minute but same hour' { + $exp = '20-40 * * * *' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 1, 15, 2, 31, 0)) + } - It 'Returns the a very specific date' { - $exp = '37 13 5 2 FRI' - $cron = ConvertFrom-PodeCronExpressions -Expressions $exp - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2021, 2, 5, 13, 37, 0)) - } + It 'Returns the a very specific date' { + $exp = '37 13 5 2 FRI' + $cron = ConvertFrom-PodeCronExpressions -Expressions $exp + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2021, 2, 5, 13, 37, 0)) + } - It 'Returns the 30 March' { - $inputDate = [datetime]::new(2020, 1, 31, 0, 0, 0) - $cron = ConvertFrom-PodeCronExpressions -Expressions '* * 30 * *' - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 3, 30, 0, 0, 0)) - } + It 'Returns the 30 March' { + $inputDate = [datetime]::new(2020, 1, 31, 0, 0, 0) + $cron = ConvertFrom-PodeCronExpressions -Expressions '* * 30 * *' + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 3, 30, 0, 0, 0)) + } - It 'Returns the 28 Feb' { - $inputDate = [datetime]::new(2020, 1, 31, 0, 0, 0) - $cron = ConvertFrom-PodeCronExpressions -Expressions '* * 28 * *' - Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should Be ([datetime]::new(2020, 2, 28, 0, 0, 0)) + It 'Returns the 28 Feb' { + $inputDate = [datetime]::new(2020, 1, 31, 0, 0, 0) + $cron = ConvertFrom-PodeCronExpressions -Expressions '* * 28 * *' + Get-PodeCronNextTrigger -Expression $cron -StartTime $inputDate | Should -Be ([datetime]::new(2020, 2, 28, 0, 0, 0)) + } } } Describe 'Get-PodeCronNextEarliestTrigger' { - $inputDate = [datetime]::new(2020, 1, 1) + BeforeEach { + $inputDate = [datetime]::new(2020, 1, 1) + } It 'Returns the earliest trigger when both valid' { $crons = ConvertFrom-PodeCronExpressions -Expressions '* * 11 * FRI', '* * 10 * WED' - Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate | Should Be ([datetime]::new(2020, 6, 10, 0, 0, 0)) + Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate | Should -Be ([datetime]::new(2020, 6, 10, 0, 0, 0)) } It 'Returns the earliest trigger when one after end time' { $end = [datetime]::new(2020, 1, 9) $crons = ConvertFrom-PodeCronExpressions -Expressions '* * 8 * WED', '* * 10 * FRi' - Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate -EndTime $end | Should Be ([datetime]::new(2020, 1, 8, 0, 0, 0)) + Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate -EndTime $end | Should -Be ([datetime]::new(2020, 1, 8, 0, 0, 0)) } It 'Returns the null when all after end time' { $end = [datetime]::new(2020, 1, 7) $crons = ConvertFrom-PodeCronExpressions -Expressions '* * 8 * WED', '* * 10 * FRi' - Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate -EndTime $end | Should Be $null + Get-PodeCronNextEarliestTrigger -Expressions $crons -StartTime $inputDate -EndTime $end | Should -Be $null } } \ No newline at end of file diff --git a/tests/unit/Cryptography.Tests.ps1 b/tests/unit/Cryptography.Tests.ps1 index 15f9d1c3d..1345986c5 100644 --- a/tests/unit/Cryptography.Tests.ps1 +++ b/tests/unit/Cryptography.Tests.ps1 @@ -1,11 +1,13 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Invoke-PodeHMACSHA256Hash' { Context 'Valid parameters' { It 'Returns encrypted data' { - Invoke-PodeHMACSHA256Hash -Value 'value' -Secret 'key' | Should Be 'kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=' + Invoke-PodeHMACSHA256Hash -Value 'value' -Secret 'key' | Should -Be 'kPv88V50o2uJ29sqch2a7P/f3dxcg+J/dZJZT3GTJIE=' } } } @@ -13,43 +15,43 @@ Describe 'Invoke-PodeHMACSHA256Hash' { Describe 'Invoke-PodeSHA256Hash' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Invoke-PodeSHA256Hash -Value $null } | Should Throw 'argument is null or empty' + { Invoke-PodeSHA256Hash -Value $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws empty value error' { - { Invoke-PodeSHA256Hash -Value '' } | Should Throw 'argument is null or empty' + { Invoke-PodeSHA256Hash -Value '' } | Should -Throw -ExpectedMessage '*argument is null or empty*' } } Context 'Valid parameters' { It 'Returns encrypted data' { - Invoke-PodeSHA256Hash -Value 'value' | Should Be 'zUJATVKtVcz6mspK3IKKpYAK2dOFoGcfvL9yQRgyBhk=' + Invoke-PodeSHA256Hash -Value 'value' | Should -Be 'zUJATVKtVcz6mspK3IKKpYAK2dOFoGcfvL9yQRgyBhk=' } } } Describe 'New-PodeGuid' { It 'Returns a valid guid' { - (New-PodeGuid) | Should Not Be $null + (New-PodeGuid) | Should -Not -Be $null } It 'Returns a secure guid' { - Mock Get-PodeRandomBytes { return @(10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10) } - New-PodeGuid -Secure -Length 16 | Should Be '0a0a0a0a-0a0a-0a0a-0a0a-0a0a0a0a0a0a' + Mock Get-PodeRandomBytes { return @(10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10) } + New-PodeGuid -Secure -Length 16 | Should -Be '0a0a0a0a-0a0a-0a0a-0a0a-0a0a0a0a0a0a' } } Describe 'Get-PodeRandomBytes' { It 'Returns an array of bytes' { $b = (Get-PodeRandomBytes -Length 16) - $b | Should Not Be $null - $b.Length | Should Be 16 + $b | Should -Not -Be $null + $b.Length | Should -Be 16 } } Describe 'New-PodeSalt' { It 'Returns a salt' { Mock Get-PodeRandomBytes { return @(10, 10, 10) } - New-PodeSalt -Length 3 | Should Be 'CgoK' + New-PodeSalt -Length 3 | Should -Be 'CgoK' } } \ No newline at end of file diff --git a/tests/unit/Endware.Tests.ps1 b/tests/unit/Endware.Tests.ps1 index 17e8cbdd3..ba7458420 100644 --- a/tests/unit/Endware.Tests.ps1 +++ b/tests/unit/Endware.Tests.ps1 @@ -1,6 +1,8 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Invoke-PodeEndware' { It 'Returns for no endware' { @@ -36,7 +38,7 @@ Describe 'Invoke-PodeEndware' { Describe 'Add-PodeEndware' { Context 'Invalid parameters supplied' { It 'Throws null logic error' { - { Add-PodeEndware -ScriptBlock $null } | Should Throw 'because it is null' + { Add-PodeEndware -ScriptBlock $null } | Should -Throw -ExpectedMessage '*because it is null*' } } @@ -46,8 +48,8 @@ Describe 'Add-PodeEndware' { Add-PodeEndware -ScriptBlock { write-host 'end1' } - $PodeContext.Server.Endware.Length | Should Be 1 - $PodeContext.Server.Endware[0].Logic.ToString() | Should Be ({ Write-Host 'end1' }).ToString() + $PodeContext.Server.Endware.Length | Should -Be 1 + $PodeContext.Server.Endware[0].Logic.ToString() | Should -Be ({ Write-Host 'end1' }).ToString() } It 'Adds two Endwares to list' { @@ -56,9 +58,9 @@ Describe 'Add-PodeEndware' { Add-PodeEndware -ScriptBlock { write-host 'end1' } Add-PodeEndware -ScriptBlock { write-host 'end2' } - $PodeContext.Server.Endware.Length | Should Be 2 - $PodeContext.Server.Endware[0].Logic.ToString() | Should Be ({ Write-Host 'end1' }).ToString() - $PodeContext.Server.Endware[1].Logic.ToString() | Should Be ({ Write-Host 'end2' }).ToString() + $PodeContext.Server.Endware.Length | Should -Be 2 + $PodeContext.Server.Endware[0].Logic.ToString() | Should -Be ({ Write-Host 'end1' }).ToString() + $PodeContext.Server.Endware[1].Logic.ToString() | Should -Be ({ Write-Host 'end2' }).ToString() } } } \ No newline at end of file diff --git a/tests/unit/Flash.Tests.ps1 b/tests/unit/Flash.Tests.ps1 index 1c53829b1..98b613611 100644 --- a/tests/unit/Flash.Tests.ps1 +++ b/tests/unit/Flash.Tests.ps1 @@ -1,21 +1,26 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Add-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Add-PodeFlashMessage -Name 'name' -Message 'message' } | Should Throw 'Sessions are required' + { Add-PodeFlashMessage -Name 'name' -Message 'message' } | Should -Throw -ExpectedMessage '*Sessions are required*' } It 'Throws error for no name supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Add-PodeFlashMessage -Name '' -Message 'message' } | Should Throw 'empty string' + { Add-PodeFlashMessage -Name '' -Message 'message' } | Should -Throw -ExpectedMessage '*empty string*' } It 'Throws error for no message supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Add-PodeFlashMessage -Name 'name' -Message '' } | Should Throw 'empty string' + { Add-PodeFlashMessage -Name 'name' -Message '' } | Should -Throw -ExpectedMessage '*empty string*' } It 'Adds a single key and value' { @@ -24,9 +29,9 @@ Describe 'Add-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 1 - $WebEvent.Session.Data.Flash['Test1'] | Should Be 'Value1' + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 1 + $WebEvent.Session.Data.Flash['Test1'] | Should -Be 'Value1' } It 'Adds two different keys and values' { @@ -36,10 +41,10 @@ Describe 'Add-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 - $WebEvent.Session.Data.Flash['Test1'] | Should Be 'Value1' - $WebEvent.Session.Data.Flash['Test2'] | Should Be 'Value2' + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 + $WebEvent.Session.Data.Flash['Test1'] | Should -Be 'Value1' + $WebEvent.Session.Data.Flash['Test2'] | Should -Be 'Value2' } It 'Adds two values for the same key' { @@ -49,18 +54,18 @@ Describe 'Add-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test1' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 1 - $WebEvent.Session.Data.Flash['Test1'].Length | Should Be 2 - $WebEvent.Session.Data.Flash['Test1'][0] | Should Be 'Value1' - $WebEvent.Session.Data.Flash['Test1'][1] | Should Be 'Value2' + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 1 + $WebEvent.Session.Data.Flash['Test1'].Length | Should -Be 2 + $WebEvent.Session.Data.Flash['Test1'][0] | Should -Be 'Value1' + $WebEvent.Session.Data.Flash['Test1'][1] | Should -Be 'Value2' } } Describe 'Clear-PodeFlashMessages' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Clear-PodeFlashMessages } | Should Throw 'Sessions are required' + { Clear-PodeFlashMessages } | Should -Throw -ExpectedMessage '*Sessions are required*' } It 'Adds two keys and then Clears them all' { @@ -70,25 +75,25 @@ Describe 'Clear-PodeFlashMessages' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 Clear-PodeFlashMessages - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 0 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 0 } } Describe 'Get-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Get-PodeFlashMessage -Name 'name' } | Should Throw 'Sessions are required' + { Get-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage '*Sessions are required*' } It 'Throws error for no key supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Get-PodeFlashMessage -Name '' } | Should Throw 'empty string' + { Get-PodeFlashMessage -Name '' } | Should -Throw -ExpectedMessage '*empty string*' } It 'Returns empty array on key that does not exist' { @@ -96,17 +101,19 @@ Describe 'Get-PodeFlashMessage' { $WebEvent = @{ 'Session' = @{ 'Data' = @{ } } } $result = (Get-PodeFlashMessage -Name 'Test1') - $result.Length | Should Be 0 + $result.Length | Should -Be 0 } It 'Returns empty array on key that is empty' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } $WebEvent = @{ 'Session' = @{ 'Data' = @{ - 'Flash' = @{ 'Test1' = @(); } - } } } + 'Flash' = @{ 'Test1' = @(); } + } + } + } $result = (Get-PodeFlashMessage -Name 'Test1') - $result.Length | Should Be 0 + $result.Length | Should -Be 0 } It 'Adds two keys and then Gets one of them' { @@ -116,14 +123,14 @@ Describe 'Get-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 $result = (Get-PodeFlashMessage -Name 'Test1') - $result | Should Be 'Value1' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 1 + $result | Should -Be 'Value1' + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 1 } It 'Adds two values for the same key then Gets it' { @@ -133,23 +140,23 @@ Describe 'Get-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test1' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 1 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 1 $result = (Get-PodeFlashMessage -Name 'Test1') - $result.Length | Should be 2 - $result[0] | Should be 'Value1' - $result[1] | Should be 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 0 + $result.Length | Should -Be 2 + $result[0] | Should -Be 'Value1' + $result[1] | Should -Be 'Value2' + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 0 } } Describe 'Get-PodeFlashMessageNames' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Get-PodeFlashMessageNames } | Should Throw 'Sessions are required' + { Get-PodeFlashMessageNames } | Should -Throw -ExpectedMessage '*Sessions are required*' } It 'Adds two keys and then retrieves the Keys' { @@ -159,17 +166,17 @@ Describe 'Get-PodeFlashMessageNames' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 $result = (Get-PodeFlashMessageNames) - $result.Length | Should Be 2 - $result.IndexOf('Test1') | Should Not Be -1 - $result.IndexOf('Test2') | Should Not Be -1 + $result.Length | Should -Be 2 + $result.IndexOf('Test1') | Should -Not -Be -1 + $result.IndexOf('Test2') | Should -Not -Be -1 - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 } It 'Returns no keys as none have been added' { @@ -177,19 +184,19 @@ Describe 'Get-PodeFlashMessageNames' { $WebEvent = @{ 'Session' = @{ 'Data' = @{ } } } $result = (Get-PodeFlashMessageNames) - $result.Length | Should Be 0 + $result.Length | Should -Be 0 } } Describe 'Remove-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Remove-PodeFlashMessage -Name 'name' } | Should Throw 'Sessions are required' + { Remove-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage '*Sessions are required*' } It 'Throws error for no key supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Remove-PodeFlashMessage -Name '' } | Should Throw 'empty string' + { Remove-PodeFlashMessage -Name '' } | Should -Throw -ExpectedMessage '*empty string*' } It 'Adds two keys and then Remove one of them' { @@ -199,25 +206,25 @@ Describe 'Remove-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 Remove-PodeFlashMessage -Name 'Test1' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 1 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 1 } } Describe 'Test-PodeFlashMessage' { It 'Throws error because sessions are not configured' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{} } } - { Test-PodeFlashMessage -Name 'name' } | Should Throw 'Sessions are required' + { Test-PodeFlashMessage -Name 'name' } | Should -Throw -ExpectedMessage '*Sessions are required*' } It 'Throws error for no key supplied' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } - { Test-PodeFlashMessage -Name '' } | Should Throw 'empty string' + { Test-PodeFlashMessage -Name '' } | Should -Throw -ExpectedMessage '*empty string*' } It 'Adds two keys and then Tests if one of them exists' { @@ -227,10 +234,10 @@ Describe 'Test-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 - Test-PodeFlashMessage -Name 'Test1' | Should Be $true + Test-PodeFlashMessage -Name 'Test1' | Should -Be $true } It 'Adds two keys and then Tests for a non-existent key' { @@ -240,15 +247,15 @@ Describe 'Test-PodeFlashMessage' { Add-PodeFlashMessage -Name 'Test1' -Message 'Value1' Add-PodeFlashMessage -Name 'Test2' -Message 'Value2' - $WebEvent.Session.Data.Flash | Should Not Be $null - $WebEvent.Session.Data.Flash.Count | Should Be 2 + $WebEvent.Session.Data.Flash | Should -Not -Be $null + $WebEvent.Session.Data.Flash.Count | Should -Be 2 - Test-PodeFlashMessage -Name 'Test3' | Should Be $false + Test-PodeFlashMessage -Name 'Test3' | Should -Be $false } It 'Returns false when no flash message have been added' { $PodeContext = @{ 'Server' = @{ 'Sessions' = @{ 'Secret' = 'Key' } } } $WebEvent = @{ 'Session' = @{ 'Data' = @{ } } } - Test-PodeFlashMessage -Name 'Test3' | Should Be $false + Test-PodeFlashMessage -Name 'Test3' | Should -Be $false } } \ No newline at end of file diff --git a/tests/unit/Handlers.Tests.ps1 b/tests/unit/Handlers.Tests.ps1 index cfea0e620..02d4d4bc7 100644 --- a/tests/unit/Handlers.Tests.ps1 +++ b/tests/unit/Handlers.Tests.ps1 @@ -1,58 +1,68 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - -$PodeContext = @{ 'Server' = $null; } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + $PodeContext = @{ 'Server' = $null; } +} Describe 'Get-PodeHandler' { Context 'Invalid parameters supplied' { It 'Throw invalid type error' { - { Get-PodeHandler -Type 'Moo' } | Should Throw "Cannot validate argument on parameter 'Type'" + { Get-PodeHandler -Type 'Moo' } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'Type'*" } } Context 'Valid parameters' { It 'Return null as type does not exist' { $PodeContext.Server = @{ 'Handlers' = @{}; } - Get-PodeHandler -Type Smtp | Should Be $null + Get-PodeHandler -Type Smtp | Should -Be $null } It 'Returns handlers for type' { $PodeContext.Server = @{ 'Handlers' = @{ 'Smtp' = @{ - 'Main' = @{ - 'Logic' = { Write-Host 'hello' }; - }; - }; }; } + 'Main' = @{ + 'Logic' = { Write-Host 'hello' } + } + } + } + } $result = (Get-PodeHandler -Type Smtp) - $result | Should Not Be $null - $result.Count | Should Be 1 - $result.Main.Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $result | Should -Not -Be $null + $result.Count | Should -Be 1 + $result.Main.Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } It 'Returns handler for type by name' { $PodeContext.Server = @{ 'Handlers' = @{ 'Smtp' = @{ - 'Main' = @{ - 'Logic' = { Write-Host 'hello' }; - }; - }; }; } + 'Main' = @{ + 'Logic' = { Write-Host 'hello' } + } + } + } + } $result = (Get-PodeHandler -Type Smtp -Name 'Main') - $result | Should Not Be $null - $result.Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $result | Should -Not -Be $null + $result.Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } It 'Returns no handler for type by name' { $PodeContext.Server = @{ 'Handlers' = @{ 'Smtp' = @{ - 'Main' = @{ - 'Logic' = { Write-Host 'hello' }; - }; - }; }; } + 'Main' = @{ + 'Logic' = { Write-Host 'hello' } + } + } + } + } $result = (Get-PodeHandler -Type Smtp -Name 'Fail') - $result | Should Be $null + $result | Should -Be $null } } } @@ -60,7 +70,7 @@ Describe 'Get-PodeHandler' { Describe 'Add-PodeHandler' { It 'Throws error because type already exists' { $PodeContext.Server = @{ 'Handlers' = @{ 'Smtp' = @{ 'Main' = @{}; }; }; } - { Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock {} } | Should Throw 'already defined' + { Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already defined*' } It 'Adds smtp handler' { @@ -68,8 +78,8 @@ Describe 'Add-PodeHandler' { Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { Write-Host 'hello' } $handler = $PodeContext.Server.Handlers['smtp'] - $handler.Count | Should Be 1 - $handler['Main'].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $handler.Count | Should -Be 1 + $handler['Main'].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } It 'Adds service handler' { @@ -77,8 +87,8 @@ Describe 'Add-PodeHandler' { Add-PodeHandler -Type Service -Name 'Main' -ScriptBlock { Write-Host 'hello' } $handler = $PodeContext.Server.Handlers['service'] - $handler.Count | Should Be 1 - $handler['Main'].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $handler.Count | Should -Be 1 + $handler['Main'].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } } @@ -90,64 +100,66 @@ Describe 'Remove-PodeHandler' { Add-PodeHandler -Type Smtp -Name 'Main2' -ScriptBlock { Write-Host 'hello2' } $handler = $PodeContext.Server.Handlers['Smtp'] - $handler.Count | Should Be 2 - $handler['Main1'].Logic.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() - $handler['Main2'].Logic.ToString() | Should Be ({ Write-Host 'hello2' }).ToString() + $handler.Count | Should -Be 2 + $handler['Main1'].Logic.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() + $handler['Main2'].Logic.ToString() | Should -Be ({ Write-Host 'hello2' }).ToString() Remove-PodeHandler -Type Smtp -Name 'Main1' $handler = $PodeContext.Server.Handlers['Smtp'] - $handler.Count | Should Be 1 - $handler['Main2'].Logic.ToString() | Should Be ({ Write-Host 'hello2' }).ToString() + $handler.Count | Should -Be 1 + $handler['Main2'].Logic.ToString() | Should -Be ({ Write-Host 'hello2' }).ToString() } } Describe 'Clear-PodeHandlers' { It 'Adds handlers, and removes them all for one type' { $PodeContext.Server = @{ 'Handlers' = @{ - 'SMTP' = @{}; - 'Service' = @{}; - }; } + 'SMTP' = @{} + 'Service' = @{} + } + } Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { Write-Host 'hello' } Add-PodeHandler -Type Service -Name 'Main' -ScriptBlock { Write-Host 'hello' } $handler = $PodeContext.Server.Handlers['smtp'] - $handler.Count | Should Be 1 + $handler.Count | Should -Be 1 $handler = $PodeContext.Server.Handlers['service'] - $handler.Count | Should Be 1 + $handler.Count | Should -Be 1 Clear-PodeHandlers -Type Smtp $handler = $PodeContext.Server.Handlers['smtp'] - $handler.Count | Should Be 0 + $handler.Count | Should -Be 0 $handler = $PodeContext.Server.Handlers['service'] - $handler.Count | Should Be 1 + $handler.Count | Should -Be 1 } It 'Adds handlers, and removes them all' { $PodeContext.Server = @{ 'Handlers' = @{ - 'SMTP' = @{}; - 'Service' = @{}; - }; } + 'SMTP' = @{} + 'Service' = @{} + } + } Add-PodeHandler -Type Smtp -Name 'Main' -ScriptBlock { Write-Host 'hello' } Add-PodeHandler -Type Service -Name 'Main' -ScriptBlock { Write-Host 'hello' } $handler = $PodeContext.Server.Handlers['smtp'] - $handler.Count | Should Be 1 + $handler.Count | Should -Be 1 $handler = $PodeContext.Server.Handlers['service'] - $handler.Count | Should Be 1 + $handler.Count | Should -Be 1 Clear-PodeHandlers $handler = $PodeContext.Server.Handlers['smtp'] - $handler.Count | Should Be 0 + $handler.Count | Should -Be 0 $handler = $PodeContext.Server.Handlers['service'] - $handler.Count | Should Be 0 + $handler.Count | Should -Be 0 } } \ No newline at end of file diff --git a/tests/unit/Headers.Tests.ps1 b/tests/unit/Headers.Tests.ps1 index d72a771ac..003671688 100644 --- a/tests/unit/Headers.Tests.ps1 +++ b/tests/unit/Headers.Tests.ps1 @@ -1,149 +1,167 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Test-PodeHeader' { Context 'WebServer' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } It 'Returns true' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = 'value' + 'Headers' = @{ + 'test' = 'value' + } } - } } + } - Test-PodeHeader -Name 'test' | Should Be $true + Test-PodeHeader -Name 'test' | Should -Be $true } It 'Returns false for no value' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } - Test-PodeHeader -Name 'test' | Should Be $false + Test-PodeHeader -Name 'test' | Should -Be $false } It 'Returns false for not existing' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } - Test-PodeHeader -Name 'test' | Should Be $false + Test-PodeHeader -Name 'test' | Should -Be $false } } Context 'Serverless' { - $PodeContext = @{ 'Server' = @{ 'Type' = 'azurefunctions' } } + BeforeEach { + $PodeContext = @{ 'Server' = @{ 'Type' = 'azurefunctions' } } } It 'Returns true' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = 'value' + 'Headers' = @{ + 'test' = 'value' + } } - } } + } - Test-PodeHeader -Name 'test' | Should Be $true + Test-PodeHeader -Name 'test' | Should -Be $true } It 'Returns false for no value' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } - Test-PodeHeader -Name 'test' | Should Be $false + Test-PodeHeader -Name 'test' | Should -Be $false } It 'Returns false for not existing' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } - Test-PodeHeader -Name 'test' | Should Be $false + Test-PodeHeader -Name 'test' | Should -Be $false } } } Describe 'Get-PodeHeader' { - Context 'WebServer' { - $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } + Context 'WebServer' { BeforeEach { + $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } } It 'Returns null for no value' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = $null + 'Headers' = @{ + 'test' = $null + } } - } } + } - Get-PodeHeader -Name 'test' | Should Be $null + Get-PodeHeader -Name 'test' | Should -Be $null } It 'Returns null for not existing' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = $null + 'Headers' = @{ + 'test' = $null + } } - } } + } - Get-PodeHeader -Name 'test' | Should Be $null + Get-PodeHeader -Name 'test' | Should -Be $null } It 'Returns a header' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = 'example' + 'Headers' = @{ + 'test' = 'example' + } } - } } + } $h = Get-PodeHeader -Name 'test' - $h | Should Be 'example' + $h | Should -Be 'example' } } Context 'Serverless' { - $PodeContext = @{ 'Server' = @{ 'Type' = 'azurefunctions' } } + BeforeEach { + $PodeContext = @{ 'Server' = @{ 'Type' = 'azurefunctions' } } } It 'Returns null for no value' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = $null + 'Headers' = @{ + 'test' = $null + } } - } } + } - Get-PodeHeader -Name 'test' | Should Be $null + Get-PodeHeader -Name 'test' | Should -Be $null } It 'Returns null for not existing' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = $null + 'Headers' = @{ + 'test' = $null + } } - } } + } - Get-PodeHeader -Name 'test' | Should Be $null + Get-PodeHeader -Name 'test' | Should -Be $null } It 'Returns a header' { $WebEvent = @{ 'Request' = @{ - 'Headers' = @{ - 'test' = 'example' + 'Headers' = @{ + 'test' = 'example' + } } - } } + } $h = Get-PodeHeader -Name 'test' - $h | Should Be 'example' + $h | Should -Be 'example' } } } Describe 'Set-PodeHeader' { Context 'WebServer' { - $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } - It 'Sets a header to response' { + $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } + $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } $WebEvent.Response.Headers | Add-Member -MemberType ScriptMethod -Name 'Set' -Value { param($n, $v) @@ -151,20 +169,21 @@ Describe 'Set-PodeHeader' { } Set-PodeHeader -Name 'test' -Value 'example' - $WebEvent.Response.Headers['test'] | Should Be 'example' + $WebEvent.Response.Headers['test'] | Should -Be 'example' } } Context 'Serverless' { - $PodeContext = @{ 'Server' = @{ ServerlessType = 'azurefunctions'; IsServerless = $true } } - It 'Sets a header to response' { + $PodeContext = @{ 'Server' = @{ ServerlessType = 'azurefunctions'; IsServerless = $true } } + $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } Set-PodeHeader -Name 'test' -Value 'example' - $WebEvent.Response.Headers['test'] | Should Be 'example' + $WebEvent.Response.Headers['test'] | Should -Be 'example' } } } @@ -172,8 +191,9 @@ Describe 'Set-PodeHeader' { Describe 'Set-PodeServerHeader' { It 'Sets the server header to response' { $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } $WebEvent.Response.Headers | Add-Member -MemberType ScriptMethod -Name 'Set' -Value { param($n, $v) @@ -181,18 +201,19 @@ Describe 'Set-PodeServerHeader' { } Set-PodeServerHeader -Type 'Example' - $WebEvent.Response.Headers['Server'] | Should Be 'Pode - Example' + $WebEvent.Response.Headers['Server'] | Should -Be 'Pode - Example' } } Describe 'Add-PodeHeader' { Context 'WebServer' { - $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } It 'Adds a header to response' { + $PodeContext = @{ 'Server' = @{ 'Type' = 'http' } } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -200,17 +221,17 @@ Describe 'Add-PodeHeader' { } Add-PodeHeader -Name 'test' -Value 'example' - $WebEvent.Response.Headers['test'] | Should Be 'example' + $WebEvent.Response.Headers['test'] | Should -Be 'example' } } Context 'Serverless' { - $PodeContext = @{ 'Server' = @{ 'Type' = 'azurefunctions' } } - It 'Adds a header to response' { + $PodeContext = @{ 'Server' = @{ 'Type' = 'azurefunctions' } } $script:WebEvent = @{ 'Response' = @{ - 'Headers' = @{} - } } + 'Headers' = @{} + } + } $WebEvent.Response | Add-Member -MemberType ScriptMethod -Name 'AppendHeader' -Value { param($n, $v) @@ -218,7 +239,7 @@ Describe 'Add-PodeHeader' { } Add-PodeHeader -Name 'test' -Value 'example' - $WebEvent.Response.Headers['test'] | Should Be 'example' + $WebEvent.Response.Headers['test'] | Should -Be 'example' } } } \ No newline at end of file diff --git a/tests/unit/Helpers.Tests.ps1 b/tests/unit/Helpers.Tests.ps1 index b39695c5b..ba0989ad2 100644 --- a/tests/unit/Helpers.Tests.ps1 +++ b/tests/unit/Helpers.Tests.ps1 @@ -1,62 +1,66 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Get-PodeType' { Context 'No value supplied' { It 'Return the null' { - Get-PodeType -Value $null | Should Be $null + Get-PodeType -Value $null | Should -Be $null } } Context 'Valid value supplied' { It 'String type' { $result = (Get-PodeType -Value [string]::Empty) - $result | Should Not Be $null - $result.Name | Should Be 'string' - $result.BaseName | Should Be 'object' + $result | Should -Not -Be $null + $result.Name | Should -Be 'string' + $result.BaseName | Should -Be 'object' } It 'Boolean type' { $result = (Get-PodeType -Value $true) - $result | Should Not Be $null - $result.Name | Should Be 'boolean' - $result.BaseName | Should Be 'valuetype' + $result | Should -Not -Be $null + $result.Name | Should -Be 'boolean' + $result.BaseName | Should -Be 'valuetype' } It 'Int32 type' { $result = (Get-PodeType -Value 1) - $result | Should Not Be $null - $result.Name | Should Be 'int32' - $result.BaseName | Should Be 'valuetype' + $result | Should -Not -Be $null + $result.Name | Should -Be 'int32' + $result.BaseName | Should -Be 'valuetype' } It 'Int64 type' { $result = (Get-PodeType -Value 1l) - $result | Should Not Be $null - $result.Name | Should Be 'int64' - $result.BaseName | Should Be 'valuetype' + $result | Should -Not -Be $null + $result.Name | Should -Be 'int64' + $result.BaseName | Should -Be 'valuetype' } It 'Hashtable type' { $result = (Get-PodeType -Value @{}) - $result | Should Not Be $null - $result.Name | Should Be 'hashtable' - $result.BaseName | Should Be 'object' + $result | Should -Not -Be $null + $result.Name | Should -Be 'hashtable' + $result.BaseName | Should -Be 'object' } It 'Array type' { $result = (Get-PodeType -Value @()) - $result | Should Not Be $null - $result.Name | Should Be 'object[]' - $result.BaseName | Should Be 'array' + $result | Should -Not -Be $null + $result.Name | Should -Be 'object[]' + $result.BaseName | Should -Be 'array' } It 'ScriptBlock type' { $result = (Get-PodeType -Value {}) - $result | Should Not Be $null - $result.Name | Should Be 'scriptblock' - $result.BaseName | Should Be 'object' + $result | Should -Not -Be $null + $result.Name | Should -Be 'scriptblock' + $result.BaseName | Should -Be 'object' } } } @@ -64,59 +68,59 @@ Describe 'Get-PodeType' { Describe 'Test-PodeIsEmpty' { Context 'No value is passed' { It 'Return true for no value' { - Test-PodeIsEmpty | Should be $true + Test-PodeIsEmpty | Should -Be $true } It 'Return true for null value' { - Test-PodeIsEmpty -Value $null | Should be $true + Test-PodeIsEmpty -Value $null | Should -Be $true } } Context 'Empty value is passed' { It 'Return true for an empty arraylist' { - Test-PodeIsEmpty -Value ([System.Collections.ArrayList]::new()) | Should Be $true + Test-PodeIsEmpty -Value ([System.Collections.ArrayList]::new()) | Should -Be $true } It 'Return true for an empty array' { - Test-PodeIsEmpty -Value @() | Should Be $true + Test-PodeIsEmpty -Value @() | Should -Be $true } It 'Return true for an empty hashtable' { - Test-PodeIsEmpty -Value @{} | Should Be $true + Test-PodeIsEmpty -Value @{} | Should -Be $true } It 'Return true for an empty string' { - Test-PodeIsEmpty -Value ([string]::Empty) | Should Be $true + Test-PodeIsEmpty -Value ([string]::Empty) | Should -Be $true } It 'Return true for a whitespace string' { - Test-PodeIsEmpty -Value " " | Should Be $true + Test-PodeIsEmpty -Value ' ' | Should -Be $true } It 'Return true for an empty scriptblock' { - Test-PodeIsEmpty -Value {} | Should Be $true + Test-PodeIsEmpty -Value {} | Should -Be $true } } Context 'Valid value is passed' { It 'Return false for a string' { - Test-PodeIsEmpty -Value "test" | Should Be $false + Test-PodeIsEmpty -Value 'test' | Should -Be $false } It 'Return false for a number' { - Test-PodeIsEmpty -Value 1 | Should Be $false + Test-PodeIsEmpty -Value 1 | Should -Be $false } It 'Return false for an array' { - Test-PodeIsEmpty -Value @('test') | Should Be $false + Test-PodeIsEmpty -Value @('test') | Should -Be $false } It 'Return false for a hashtable' { - Test-PodeIsEmpty -Value @{'key'='value';} | Should Be $false + Test-PodeIsEmpty -Value @{'key' = 'value'; } | Should -Be $false } It 'Return false for a scriptblock' { - Test-PodeIsEmpty -Value { write-host '' } | Should Be $false + Test-PodeIsEmpty -Value { write-host '' } | Should -Be $false } } } @@ -124,21 +128,21 @@ Describe 'Test-PodeIsEmpty' { Describe 'Get-PodePSVersionTable' { It 'Returns valid hashtable' { $table = Get-PodePSVersionTable - $table | Should Not Be $null - $table | Should BeOfType System.Collections.Hashtable + $table | Should -Not -Be $null + $table | Should -BeOfType System.Collections.Hashtable } } Describe 'Test-PodeIsUnix' { It 'Returns false for non-unix' { Mock Get-PodePSVersionTable { return @{ 'Platform' = 'Windows' } } - Test-PodeIsUnix | Should Be $false + Test-PodeIsUnix | Should -Be $false Assert-MockCalled Get-PodePSVersionTable -Times 1 } It 'Returns true for unix' { Mock Get-PodePSVersionTable { return @{ 'Platform' = 'Unix' } } - Test-PodeIsUnix | Should Be $true + Test-PodeIsUnix | Should -Be $true Assert-MockCalled Get-PodePSVersionTable -Times 1 } } @@ -146,19 +150,19 @@ Describe 'Test-PodeIsUnix' { Describe 'Test-PodeIsWindows' { It 'Returns false for non-windows' { Mock Get-PodePSVersionTable { return @{ 'Platform' = 'Unix' } } - Test-PodeIsWindows | Should Be $false + Test-PodeIsWindows | Should -Be $false Assert-MockCalled Get-PodePSVersionTable -Times 1 } It 'Returns true for windows and desktop' { Mock Get-PodePSVersionTable { return @{ 'PSEdition' = 'Desktop' } } - Test-PodeIsWindows | Should Be $true + Test-PodeIsWindows | Should -Be $true Assert-MockCalled Get-PodePSVersionTable -Times 1 } It 'Returns true for windows and core' { Mock Get-PodePSVersionTable { return @{ 'Platform' = 'Win32NT'; 'PSEdition' = 'Core' } } - Test-PodeIsWindows | Should Be $true + Test-PodeIsWindows | Should -Be $true Assert-MockCalled Get-PodePSVersionTable -Times 1 } } @@ -166,87 +170,87 @@ Describe 'Test-PodeIsWindows' { Describe 'Test-PodeIsPSCore' { It 'Returns false for non-core' { Mock Get-PodePSVersionTable { return @{ 'PSEdition' = 'Desktop' } } - Test-PodeIsPSCore | Should Be $false + Test-PodeIsPSCore | Should -Be $false Assert-MockCalled Get-PodePSVersionTable -Times 1 } It 'Returns true for unix' { Mock Get-PodePSVersionTable { return @{ 'PSEdition' = 'Core' } } - Test-PodeIsPSCore | Should Be $true + Test-PodeIsPSCore | Should -Be $true Assert-MockCalled Get-PodePSVersionTable -Times 1 } } Describe 'Get-PodeHostIPRegex' { It 'Returns valid Hostname regex' { - Get-PodeHostIPRegex -Type Hostname | Should Be '(?(([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+))' + Get-PodeHostIPRegex -Type Hostname | Should -Be '(?(([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+))' } It 'Returns valid IP regex' { - Get-PodeHostIPRegex -Type IP | Should Be '(?(\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all))' + Get-PodeHostIPRegex -Type IP | Should -Be '(?(\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all))' } It 'Returns valid IP and Hostname regex' { - Get-PodeHostIPRegex -Type Both | Should Be '(?(\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all|([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+))' + Get-PodeHostIPRegex -Type Both | Should -Be '(?(\[[a-f0-9\:]+\]|((\d+\.){3}\d+)|\:\:\d*|\*|all|([a-z]|\*\.)(([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]*[a-z0-9])+))' } } Describe 'Get-PortRegex' { It 'Returns valid port regex' { - Get-PortRegex | Should Be '(?\d+)' + Get-PortRegex | Should -Be '(?\d+)' } } Describe 'Test-PodeIPAddress' { Context 'Values that are for any IP' { It 'Returns true for no value' { - Test-PodeIPAddress -IP $null | Should Be $true + Test-PodeIPAddress -IP $null | Should -Be $true } It 'Returns true for empty value' { - Test-PodeIPAddress -IP ([string]::Empty) | Should Be $true + Test-PodeIPAddress -IP ([string]::Empty) | Should -Be $true } It 'Returns true for asterisk' { - Test-PodeIPAddress -IP '*' | Should Be $true + Test-PodeIPAddress -IP '*' | Should -Be $true } It 'Returns true for all' { - Test-PodeIPAddress -IP 'all' | Should Be $true + Test-PodeIPAddress -IP 'all' | Should -Be $true } } Context 'Values for Hostnames' { It 'Returns true for valid Hostname' { - Test-PodeIPAddress -IP 'foo.com' | Should Be $true + Test-PodeIPAddress -IP 'foo.com' | Should -Be $true } It 'Returns false for invalid Hostname' { - Test-PodeIPAddress -IP '~fake.net' | Should Be $false + Test-PodeIPAddress -IP '~fake.net' | Should -Be $false } } Context 'Values for IPv4' { It 'Returns true for valid IP' { - Test-PodeIPAddress -IP '127.0.0.1' | Should Be $true + Test-PodeIPAddress -IP '127.0.0.1' | Should -Be $true } It 'Returns false for invalid IP' { - Test-PodeIPAddress -IP '256.0.0.0' | Should Be $false + Test-PodeIPAddress -IP '256.0.0.0' | Should -Be $false } } Context 'Values for IPv6' { It 'Returns true for valid shorthand IP' { - Test-PodeIPAddress -IP '[::]' | Should Be $true + Test-PodeIPAddress -IP '[::]' | Should -Be $true } It 'Returns true for valid IP' { - Test-PodeIPAddress -IP '[0000:1111:2222:3333:4444:5555:6666:7777]' | Should Be $true + Test-PodeIPAddress -IP '[0000:1111:2222:3333:4444:5555:6666:7777]' | Should -Be $true } It 'Returns false for invalid IP' { - Test-PodeIPAddress -IP '[]' | Should Be $false + Test-PodeIPAddress -IP '[]' | Should -Be $false } } } @@ -254,7 +258,7 @@ Describe 'Test-PodeIPAddress' { Describe 'ConvertTo-PodeIPAddress' { Context 'Null values' { It 'Throws error for null' { - { ConvertTo-PodeIPAddress -Address $null } | Should Throw 'the argument is null' + { ConvertTo-PodeIPAddress -Address $null } | Should -Throw -ExpectedMessage '*the argument is null*' } } @@ -262,16 +266,16 @@ Describe 'ConvertTo-PodeIPAddress' { It 'Returns IPAddress from IPEndpoint' { $_a = [System.Net.IPAddress]::Parse('127.0.0.1') $addr = ConvertTo-PodeIPAddress -Address ([System.Net.IPEndpoint]::new($_a, 8080)) - $addr | Should Not Be $null - $addr.ToString() | Should Be '127.0.0.1' + $addr | Should -Not -Be $null + $addr.ToString() | Should -Be '127.0.0.1' } It 'Returns IPAddress from Endpoint' { $_a = [System.Net.IPAddress]::Parse('127.0.0.1') $_a = [System.Net.IPEndpoint]::new($_a, 8080) $addr = ConvertTo-PodeIPAddress -Address ([System.Net.Endpoint]$_a) - $addr | Should Not Be $null - $addr.ToString() | Should Be '127.0.0.1' + $addr | Should -Not -Be $null + $addr.ToString() | Should -Be '127.0.0.1' } } } @@ -279,27 +283,27 @@ Describe 'ConvertTo-PodeIPAddress' { Describe 'Test-PodeIPAddressLocal' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressLocal -IP ([string]::Empty) } | Should Throw 'because it is an empty' + { Test-PodeIPAddressLocal -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*because it is an empty*' } It 'Throws error for null' { - { Test-PodeIPAddressLocal -IP $null } | Should Throw 'because it is an empty' + { Test-PodeIPAddressLocal -IP $null } | Should -Throw -ExpectedMessage '*because it is an empty*' } } Context 'Values not localhost' { It 'Returns false for non-localhost IP' { - Test-PodeIPAddressLocal -IP '192.168.10.10' | Should Be $false + Test-PodeIPAddressLocal -IP '192.168.10.10' | Should -Be $false } } Context 'Values that are localhost' { It 'Returns true for 127.0.0.1' { - Test-PodeIPAddressLocal -IP '127.0.0.1' | Should Be $true + Test-PodeIPAddressLocal -IP '127.0.0.1' | Should -Be $true } It 'Returns true for localhost' { - Test-PodeIPAddressLocal -IP 'localhost' | Should Be $true + Test-PodeIPAddressLocal -IP 'localhost' | Should -Be $true } } } @@ -307,35 +311,35 @@ Describe 'Test-PodeIPAddressLocal' { Describe 'Test-PodeIPAddressLocalOrAny' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressLocalOrAny -IP ([string]::Empty) } | Should Throw 'because it is an empty' + { Test-PodeIPAddressLocalOrAny -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*because it is an empty*' } It 'Throws error for null' { - { Test-PodeIPAddressLocalOrAny -IP $null } | Should Throw 'because it is an empty' + { Test-PodeIPAddressLocalOrAny -IP $null } | Should -Throw -ExpectedMessage '*because it is an empty*' } } Context 'Values not localhost' { It 'Returns false for non-localhost IP' { - Test-PodeIPAddressLocalOrAny -IP '192.168.10.10' | Should Be $false + Test-PodeIPAddressLocalOrAny -IP '192.168.10.10' | Should -Be $false } } Context 'Values that are localhost' { It 'Returns true for 0.0.0.0' { - Test-PodeIPAddressLocalOrAny -IP '0.0.0.0' | Should Be $true + Test-PodeIPAddressLocalOrAny -IP '0.0.0.0' | Should -Be $true } It 'Returns true for asterisk' { - Test-PodeIPAddressLocalOrAny -IP '*' | Should Be $true + Test-PodeIPAddressLocalOrAny -IP '*' | Should -Be $true } It 'Returns true for all' { - Test-PodeIPAddressLocalOrAny -IP 'all' | Should Be $true + Test-PodeIPAddressLocalOrAny -IP 'all' | Should -Be $true } It 'Returns true for 127.0.0.1' { - Test-PodeIPAddressLocalOrAny -IP '127.0.0.1' | Should Be $true + Test-PodeIPAddressLocalOrAny -IP '127.0.0.1' | Should -Be $true } } } @@ -343,31 +347,31 @@ Describe 'Test-PodeIPAddressLocalOrAny' { Describe 'Test-PodeIPAddressAny' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressAny -IP ([string]::Empty) } | Should Throw 'because it is an empty' + { Test-PodeIPAddressAny -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*because it is an empty*' } It 'Throws error for null' { - { Test-PodeIPAddressAny -IP $null } | Should Throw 'because it is an empty' + { Test-PodeIPAddressAny -IP $null } | Should -Throw -ExpectedMessage '*because it is an empty*' } } Context 'Values not any' { It 'Returns false for non-any IP' { - Test-PodeIPAddressAny -IP '192.168.10.10' | Should Be $false + Test-PodeIPAddressAny -IP '192.168.10.10' | Should -Be $false } } Context 'Values that are any' { It 'Returns true for 0.0.0.0' { - Test-PodeIPAddressAny -IP '0.0.0.0' | Should Be $true + Test-PodeIPAddressAny -IP '0.0.0.0' | Should -Be $true } It 'Returns true for asterisk' { - Test-PodeIPAddressAny -IP '*' | Should Be $true + Test-PodeIPAddressAny -IP '*' | Should -Be $true } It 'Returns true for all' { - Test-PodeIPAddressAny -IP 'all' | Should Be $true + Test-PodeIPAddressAny -IP 'all' | Should -Be $true } } } @@ -375,53 +379,53 @@ Describe 'Test-PodeIPAddressAny' { Describe 'Get-PodeIPAddress' { Context 'Values that are for any IP' { It 'Returns any IP for no value' { - (Get-PodeIPAddress -IP $null).ToString() | Should Be '0.0.0.0' + (Get-PodeIPAddress -IP $null).ToString() | Should -Be '0.0.0.0' } It 'Returns any IP for empty value' { - (Get-PodeIPAddress -IP ([string]::Empty)).ToString() | Should Be '0.0.0.0' + (Get-PodeIPAddress -IP ([string]::Empty)).ToString() | Should -Be '0.0.0.0' } It 'Returns any IP for asterisk' { - (Get-PodeIPAddress -IP '*').ToString() | Should Be '0.0.0.0' + (Get-PodeIPAddress -IP '*').ToString() | Should -Be '0.0.0.0' } It 'Returns any IP for all' { - (Get-PodeIPAddress -IP 'all').ToString() | Should Be '0.0.0.0' + (Get-PodeIPAddress -IP 'all').ToString() | Should -Be '0.0.0.0' } } Context 'Values for Hostnames' { It 'Returns Hostname for valid Hostname' { - (Get-PodeIPAddress -IP 'foo.com').ToString() | Should Be 'foo.com' + (Get-PodeIPAddress -IP 'foo.com').ToString() | Should -Be 'foo.com' } It 'Throws error for invalid IP' { - { Get-PodeIPAddress -IP '~fake.net' } | Should Throw 'invalid ip address' + { Get-PodeIPAddress -IP '~fake.net' } | Should -Throw -ExpectedMessage '*invalid IP address*' } } Context 'Values for IPv4' { It 'Returns IP for valid IP' { - (Get-PodeIPAddress -IP '127.0.0.1').ToString() | Should Be '127.0.0.1' + (Get-PodeIPAddress -IP '127.0.0.1').ToString() | Should -Be '127.0.0.1' } It 'Throws error for invalid IP' { - { Get-PodeIPAddress -IP '256.0.0.0' } | Should Throw 'invalid ip address' + { Get-PodeIPAddress -IP '256.0.0.0' } | Should -Throw -ExpectedMessage '*invalid IP address*' } } Context 'Values for IPv6' { It 'Returns IP for valid shorthand IP' { - (Get-PodeIPAddress -IP '[::]').ToString() | Should Be '::' + (Get-PodeIPAddress -IP '[::]').ToString() | Should -Be '::' } It 'Returns IP for valid IP' { - (Get-PodeIPAddress -IP '[0000:1111:2222:3333:4444:5555:6666:7777]').ToString() | Should Be '0:1111:2222:3333:4444:5555:6666:7777' + (Get-PodeIPAddress -IP '[0000:1111:2222:3333:4444:5555:6666:7777]').ToString() | Should -Be '0:1111:2222:3333:4444:5555:6666:7777' } It 'Throws error for invalid IP' { - { Get-PodeIPAddress -IP '[]' } | Should Throw 'invalid ip address' + { Get-PodeIPAddress -IP '[]' } | Should -Throw -ExpectedMessage '*invalid IP address*' } } } @@ -429,15 +433,15 @@ Describe 'Get-PodeIPAddress' { Describe 'Test-PodeIPAddressInRange' { Context 'No parameters supplied' { It 'Throws error for no ip' { - { Test-PodeIPAddressInRange -IP $null -LowerIP @{} -UpperIP @{} } | Should Throw 'because it is null' + { Test-PodeIPAddressInRange -IP $null -LowerIP @{} -UpperIP @{} } | Should -Throw -ExpectedMessage '*because it is null*' } It 'Throws error for no lower ip' { - { Test-PodeIPAddressInRange -IP @{} -LowerIP $null -UpperIP @{} } | Should Throw 'because it is null' + { Test-PodeIPAddressInRange -IP @{} -LowerIP $null -UpperIP @{} } | Should -Throw -ExpectedMessage '*because it is null*' } It 'Throws error for no upper ip' { - { Test-PodeIPAddressInRange -IP @{} -LowerIP @{} -UpperIP $null } | Should Throw 'because it is null' + { Test-PodeIPAddressInRange -IP @{} -LowerIP @{} -UpperIP $null } | Should -Throw -ExpectedMessage '*because it is null*' } } @@ -446,49 +450,49 @@ Describe 'Test-PodeIPAddressInRange' { $ip = @{ 'Bytes' = @(127, 0, 0, 4); 'Family' = 'different' } $lower = @{ 'Bytes' = @(127, 0, 0, 2); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 10); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $false + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $false } It 'Returns false because ip is above range' { $ip = @{ 'Bytes' = @(127, 0, 0, 11); 'Family' = 'test' } $lower = @{ 'Bytes' = @(127, 0, 0, 2); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 10); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $false + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $false } It 'Returns false because ip is under range' { $ip = @{ 'Bytes' = @(127, 0, 0, 1); 'Family' = 'test' } $lower = @{ 'Bytes' = @(127, 0, 0, 2); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 10); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $false + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $false } It 'Returns true because ip is in range' { $ip = @{ 'Bytes' = @(127, 0, 0, 4); 'Family' = 'test' } $lower = @{ 'Bytes' = @(127, 0, 0, 2); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 10); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $true + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $true } It 'Returns false because ip is above range, bounds are same' { $ip = @{ 'Bytes' = @(127, 0, 0, 11); 'Family' = 'test' } $lower = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $false + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $false } It 'Returns false because ip is under range, bounds are same' { $ip = @{ 'Bytes' = @(127, 0, 0, 1); 'Family' = 'test' } $lower = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $false + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $false } It 'Returns true because ip is in range, bounds are same' { $ip = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } $lower = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } $upper = @{ 'Bytes' = @(127, 0, 0, 5); 'Family' = 'test' } - Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should Be $true + Test-PodeIPAddressInRange -IP $ip -LowerIP $lower -UpperIP $upper | Should -Be $true } } } @@ -496,21 +500,21 @@ Describe 'Test-PodeIPAddressInRange' { Describe 'Test-PodeIPAddressIsSubnetMask' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodeIPAddressIsSubnetMask -IP ([string]::Empty) } | Should Throw 'argument is null or empty' + { Test-PodeIPAddressIsSubnetMask -IP ([string]::Empty) } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws error for null' { - { Test-PodeIPAddressIsSubnetMask -IP $null } | Should Throw 'argument is null or empty' + { Test-PodeIPAddressIsSubnetMask -IP $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' } } Context 'Valid parameters' { It 'Returns false for non-subnet' { - Test-PodeIPAddressIsSubnetMask -IP '127.0.0.1' | Should Be $false + Test-PodeIPAddressIsSubnetMask -IP '127.0.0.1' | Should -Be $false } It 'Returns true for subnet' { - Test-PodeIPAddressIsSubnetMask -IP '10.10.0.0/24' | Should Be $true + Test-PodeIPAddressIsSubnetMask -IP '10.10.0.0/24' | Should -Be $true } } } @@ -519,11 +523,11 @@ Describe 'Get-PodeSubnetRange' { Context 'Valid parameter supplied' { It 'Returns valid subnet range' { $range = Get-PodeSubnetRange -SubnetMask '10.10.0.0/24' - $range.Lower | Should Be '10.10.0.0' - $range.Upper | Should Be '10.10.0.255' - $range.Range | Should Be '0.0.0.255' - $range.Netmask | Should Be '255.255.255.0' - $range.IP | Should Be '10.10.0.0' + $range.Lower | Should -Be '10.10.0.0' + $range.Upper | Should -Be '10.10.0.255' + $range.Range | Should -Be '0.0.0.255' + $range.Netmask | Should -Be '255.255.255.0' + $range.IP | Should -Be '10.10.0.0' } } } @@ -531,11 +535,11 @@ Describe 'Get-PodeSubnetRange' { Describe 'Resolve-PodeValue' { Context 'Valid values' { It 'Returns Value2 for False Check' { - Resolve-PodeValue -Check $false -TrueValue 'test' -FalseValue 'hello' | Should Be 'hello' + Resolve-PodeValue -Check $false -TrueValue 'test' -FalseValue 'hello' | Should -Be 'hello' } It 'Returns Value1 for True Check' { - Resolve-PodeValue -Check $true -TrueValue 'test' -FalseValue 'hello' | Should Be 'test' + Resolve-PodeValue -Check $true -TrueValue 'test' -FalseValue 'hello' | Should -Be 'test' } } } @@ -543,19 +547,19 @@ Describe 'Resolve-PodeValue' { Describe 'Get-PodeFileExtension' { Context 'Valid values' { It 'Returns extension for file' { - Get-PodeFileExtension -Path 'test.txt' | Should Be '.txt' + Get-PodeFileExtension -Path 'test.txt' | Should -Be '.txt' } It 'Returns extension for file with no period' { - Get-PodeFileExtension -Path 'test.txt' -TrimPeriod | Should Be 'txt' + Get-PodeFileExtension -Path 'test.txt' -TrimPeriod | Should -Be 'txt' } It 'Returns extension for path' { - Get-PodeFileExtension -Path 'this/is/some/test.txt' | Should Be '.txt' + Get-PodeFileExtension -Path 'this/is/some/test.txt' | Should -Be '.txt' } It 'Returns extension for path with no period' { - Get-PodeFileExtension -Path 'this/is/some/test.txt' -TrimPeriod | Should Be 'txt' + Get-PodeFileExtension -Path 'this/is/some/test.txt' -TrimPeriod | Should -Be 'txt' } } } @@ -563,19 +567,19 @@ Describe 'Get-PodeFileExtension' { Describe 'Get-PodeFileName' { Context 'Valid values' { It 'Returns name for file with extension' { - Get-PodeFileName -Path 'test.txt' | Should Be 'test.txt' + Get-PodeFileName -Path 'test.txt' | Should -Be 'test.txt' } It 'Returns name for file with no period with extension' { - Get-PodeFileName -Path 'test.txt' -WithoutExtension | Should Be 'test' + Get-PodeFileName -Path 'test.txt' -WithoutExtension | Should -Be 'test' } It 'Returns name for path' { - Get-PodeFileName -Path 'this/is/some/test.txt' | Should Be 'test.txt' + Get-PodeFileName -Path 'this/is/some/test.txt' | Should -Be 'test.txt' } It 'Returns name for path with no period with extension' { - Get-PodeFileName -Path 'this/is/some/test.txt' -WithoutExtension | Should Be 'test' + Get-PodeFileName -Path 'this/is/some/test.txt' -WithoutExtension | Should -Be 'test' } } } @@ -584,27 +588,27 @@ Describe 'Test-PodeValidNetworkFailure' { Context 'Valid values' { It 'Returns true for network name' { $ex = @{ 'Message' = 'the network name is no longer available for use' } - Test-PodeValidNetworkFailure -Exception $ex | Should Be $true + Test-PodeValidNetworkFailure -Exception $ex | Should -Be $true } It 'Returns true for network connection' { $ex = @{ 'Message' = 'a nonexistent network connection was detected' } - Test-PodeValidNetworkFailure -Exception $ex | Should Be $true + Test-PodeValidNetworkFailure -Exception $ex | Should -Be $true } It 'Returns true for network pipe' { $ex = @{ 'Message' = 'network connection fail: broken pipe' } - Test-PodeValidNetworkFailure -Exception $ex | Should Be $true + Test-PodeValidNetworkFailure -Exception $ex | Should -Be $true } It 'Returns false for empty' { $ex = @{ 'Message' = '' } - Test-PodeValidNetworkFailure -Exception $ex | Should Be $false + Test-PodeValidNetworkFailure -Exception $ex | Should -Be $false } It 'Returns false for null' { $ex = @{ 'Message' = $null } - Test-PodeValidNetworkFailure -Exception $ex | Should Be $false + Test-PodeValidNetworkFailure -Exception $ex | Should -Be $false } } } @@ -616,13 +620,13 @@ Describe 'ConvertFrom-PodeRequestContent' { $value = 'test' $result = ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'text/xml' - $result.Data | Should Not Be $null - $result.Data.root | Should Not Be $null - $result.Data.root.value | Should Be 'test' + $result.Data | Should -Not -Be $null + $result.Data.root | Should -Not -Be $null + $result.Data.root.value | Should -Be 'test' } It 'Returns json data' { @@ -630,12 +634,12 @@ Describe 'ConvertFrom-PodeRequestContent' { $value = '{ "value": "test" }' $result = ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'application/json' - $result.Data | Should Not Be $null - $result.Data.value | Should Be 'test' + $result.Data | Should -Not -Be $null + $result.Data.value | Should -Be 'test' } It 'Returns csv data' { @@ -643,46 +647,46 @@ Describe 'ConvertFrom-PodeRequestContent' { $value = "value`ntest" $result = ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 } -ContentType 'text/csv' - $result | Should Not Be $null - $result.Data[0].value | Should Be 'test' + $result | Should -Not -Be $null + $result.Data[0].value | Should -Be 'test' } It 'Returns original data' { $PodeContext = @{ 'Server' = @{ 'Type' = 'http'; 'BodyParsers' = @{} } } - $value = "test" + $value = 'test' (ConvertFrom-PodeRequestContent -Request @{ - Body = $value + Body = $value ContentEncoding = [System.Text.Encoding]::UTF8 - } -ContentType 'text/custom').Data | Should Be 'test' + } -ContentType 'text/custom').Data | Should -Be 'test' } It 'Returns json data for azure-functions' { $PodeContext = @{ 'Server' = @{ 'ServerlessType' = 'AzureFunctions'; 'BodyParsers' = @{}; 'IsServerless' = $true } } $result = ConvertFrom-PodeRequestContent -Request @{ - 'ContentEncoding' = [System.Text.Encoding]::UTF8; - 'RawBody' = '{ "value": "test" }'; + 'ContentEncoding' = [System.Text.Encoding]::UTF8 + 'RawBody' = '{ "value": "test" }' } -ContentType 'application/json' - $result.Data | Should Not Be $null - $result.Data.value | Should Be 'test' + $result.Data | Should -Not -Be $null + $result.Data.value | Should -Be 'test' } It 'Returns json data for aws-lambda' { $PodeContext = @{ 'Server' = @{ 'ServerlessType' = 'AwsLambda'; 'BodyParsers' = @{}; 'IsServerless' = $true } } $result = ConvertFrom-PodeRequestContent -Request @{ - 'ContentEncoding' = [System.Text.Encoding]::UTF8; - 'body' = '{ "value": "test" }'; + 'ContentEncoding' = [System.Text.Encoding]::UTF8 + 'body' = '{ "value": "test" }' } -ContentType 'application/json' - $result.Data | Should Not Be $null - $result.Data.value | Should Be 'test' + $result.Data | Should -Not -Be $null + $result.Data.value | Should -Be 'test' } } } @@ -690,163 +694,163 @@ Describe 'ConvertFrom-PodeRequestContent' { Describe 'Test-PodePathIsFile' { Context 'Valid values' { It 'Returns true for a file' { - Test-PodePathIsFile -Path './some/path/file.txt' | Should Be $true + Test-PodePathIsFile -Path './some/path/file.txt' | Should -Be $true } It 'Returns false for a directory' { - Test-PodePathIsFile -Path './some/path/folder' | Should Be $false + Test-PodePathIsFile -Path './some/path/folder' | Should -Be $false } It 'Returns false for a wildcard' { - Test-PodePathIsFile -Path './some/path/*' -FailOnWildcard | Should Be $false + Test-PodePathIsFile -Path './some/path/*' -FailOnWildcard | Should -Be $false } } } Describe 'Test-PodePathIsWildcard' { - It 'Returns true for a wildcard' { - Test-PodePathIsWildcard -Path './some/path/*' | Should Be $true - } + It 'Returns true for a wildcard' { + Test-PodePathIsWildcard -Path './some/path/*' | Should -Be $true + } - It 'Returns false for no wildcard' { - Test-PodePathIsWildcard -Path './some/path/folder' | Should Be $false - } + It 'Returns false for no wildcard' { + Test-PodePathIsWildcard -Path './some/path/folder' | Should -Be $false + } } Describe 'Test-PodePathIsDirectory' { Context 'Null values' { It 'Throws error for empty' { - { Test-PodePathIsDirectory -Path ([string]::Empty) } | Should Throw 'argument is null or empty' + { Test-PodePathIsDirectory -Path ([string]::Empty) } | Should -Throw -ExpectedMessage '*argument is null or empty*' } It 'Throws error for null' { - { Test-PodePathIsDirectory -Path $null } | Should Throw 'argument is null or empty' + { Test-PodePathIsDirectory -Path $null } | Should -Throw -ExpectedMessage '*argument is null or empty*' } } Context 'Valid values' { It 'Returns true for a directory' { - Test-PodePathIsDirectory -Path './some/path/folder' | Should Be $true + Test-PodePathIsDirectory -Path './some/path/folder' | Should -Be $true } It 'Returns false for a file' { - Test-PodePathIsDirectory -Path './some/path/file.txt' | Should Be $false + Test-PodePathIsDirectory -Path './some/path/file.txt' | Should -Be $false } It 'Returns false for a wildcard' { - Test-PodePathIsDirectory -Path './some/path/*' -FailOnWildcard | Should Be $false + Test-PodePathIsDirectory -Path './some/path/*' -FailOnWildcard | Should -Be $false } } } Describe 'Remove-PodeEmptyItemsFromArray' { It 'Returns an empty array for no array passed' { - Remove-PodeEmptyItemsFromArray @() | Should Be @() + Remove-PodeEmptyItemsFromArray @() | Should -Be @() } It 'Returns an empty array for an array of empty items' { - Remove-PodeEmptyItemsFromArray @('', $null) | Should Be @() + Remove-PodeEmptyItemsFromArray @('', $null) | Should -Be @() } It 'Returns a single item array' { - Remove-PodeEmptyItemsFromArray @('app', '', $null) | Should Be @('app') + Remove-PodeEmptyItemsFromArray @('app', '', $null) | Should -Be @('app') } It 'Returns a multi item array' { - Remove-PodeEmptyItemsFromArray @('app', 'test', '', $null) | Should Be @('app', 'test') + Remove-PodeEmptyItemsFromArray @('app', 'test', '', $null) | Should -Be @('app', 'test') } } Describe 'Get-PodeEndpointInfo' { It 'Returns null for no endpoint' { - Get-PodeEndpointInfo -Address ([string]::Empty) | Should Be $null + Get-PodeEndpointInfo -Address ([string]::Empty) | Should -Be $null } It 'Throws an error for an invalid IP endpoint' { - { Get-PodeEndpointInfo -Address '700.0.0.a' } | Should Throw 'Failed to parse' + { Get-PodeEndpointInfo -Address '700.0.0.a' } | Should -Throw -ExpectedMessage '*Failed to parse*' } It 'Throws an error for an out-of-range IP endpoint' { - { Get-PodeEndpointInfo -Address '700.0.0.0' } | Should Throw 'The IP address supplied is invalid' + { Get-PodeEndpointInfo -Address '700.0.0.0' } | Should -Throw -ExpectedMessage '*The IP address supplied is invalid*' } It 'Throws an error for an invalid Hostname endpoint' { - { Get-PodeEndpointInfo -Address '@test.host.com' } | Should Throw 'Failed to parse' + { Get-PodeEndpointInfo -Address '@test.host.com' } | Should -Throw -ExpectedMessage '*Failed to parse*' } } Describe 'Test-PodeHostname' { It 'Returns true for a valid hostname' { - Test-PodeHostname -Hostname 'test.host.com' | Should Be $true + Test-PodeHostname -Hostname 'test.host.com' | Should -Be $true } It 'Returns false for a valid hostname' { - Test-PodeHostname -Hostname 'test.ho@st.com' | Should Be $false + Test-PodeHostname -Hostname 'test.ho@st.com' | Should -Be $false } } Describe 'Remove-PodeEmptyItemsFromArray' { It 'Returns an empty array for no entries' { - Remove-PodeEmptyItemsFromArray -Array @() | Should Be @() + Remove-PodeEmptyItemsFromArray -Array @() | Should -Be @() } It 'Returns en empty array for an array with null entries' { - Remove-PodeEmptyItemsFromArray -Array @($null) | Should Be @() + Remove-PodeEmptyItemsFromArray -Array @($null) | Should -Be @() } It 'Filters out the null entries' { - Remove-PodeEmptyItemsFromArray -Array @('bill', $null, 'bob') | Should Be @('bill', 'bob') + Remove-PodeEmptyItemsFromArray -Array @('bill', $null, 'bob') | Should -Be @('bill', 'bob') } It 'Returns an empty array for a null array' { - Remove-PodeEmptyItemsFromArray -Array $null | Should Be @() + Remove-PodeEmptyItemsFromArray -Array $null | Should -Be @() } } Describe 'Invoke-PodeScriptBlock' { It 'Runs scriptblock unscoped, unsplatted, no-args' { - Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Return | Should Be 7 + Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Return | Should -Be 7 } It 'Runs scriptblock unscoped, unsplatted, no-args, force closure for serverless' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } - Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Return | Should Be 7 + Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Return | Should -Be 7 } It 'Runs scriptblock unscoped, unsplatted, args' { - Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Arguments 5 -Return | Should Be 5 + Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Arguments 5 -Return | Should -Be 5 } It 'Runs scriptblock scoped, unsplatted, no-args' { - Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Scoped -Return | Should Be 7 + Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Scoped -Return | Should -Be 7 } It 'Runs scriptblock scoped, unsplatted, args' { - Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Scoped -Arguments 5 -Return | Should Be 5 + Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Scoped -Arguments 5 -Return | Should -Be 5 } It 'Runs scriptblock unscoped, splatted, no-args' { - Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Splat -Return | Should Be 7 + Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Splat -Return | Should -Be 7 } It 'Runs scriptblock unscoped, splatted, args' { - Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Splat -Arguments @(5) -Return | Should Be 5 + Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Splat -Arguments @(5) -Return | Should -Be 5 } It 'Runs scriptblock scoped, splatted, no-args' { - Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Scoped -Splat -Return | Should Be 7 + Invoke-PodeScriptBlock -ScriptBlock { return 7 } -Scoped -Splat -Return | Should -Be 7 } It 'Runs scriptblock scoped, splatted, args' { - Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Scoped -Splat -Arguments @(5) -Return | Should Be 5 + Invoke-PodeScriptBlock -ScriptBlock { param($i) return $i } -Scoped -Splat -Arguments @(5) -Return | Should -Be 5 } } Describe 'ConvertFrom-PodeNameValueToHashTable' { It 'Returns an empty hashtable for no collection' { $result = ConvertFrom-PodeNameValueToHashTable -Collection $null - ($result -is [hashtable]) | Should Be $true - $result.Count | Should Be 0 + ($result -is [hashtable]) | Should -Be $true + $result.Count | Should -Be 0 } It 'Returns a hashtable from a NameValue collection' { @@ -854,17 +858,17 @@ Describe 'ConvertFrom-PodeNameValueToHashTable' { $c.Add('colour', 'blue') $r = ConvertFrom-PodeNameValueToHashTable -Collection $c - $r.GetType().Name | Should Be 'Hashtable' - $r.colour.GetType().Name | Should Be 'string' - $r.colour | Should Be 'blue' + $r.GetType().Name | Should -Be 'Hashtable' + $r.colour.GetType().Name | Should -Be 'string' + $r.colour | Should -Be 'blue' } It 'Returns a hashtable from a value without key collection' { $c = [System.Web.HttpUtility]::ParseQueryString('?blue') $r = ConvertFrom-PodeNameValueToHashTable -Collection $c - $r.GetType().Name | Should Be 'Hashtable' - $r.'' | Should Be 'blue' + $r.GetType().Name | Should -Be 'Hashtable' + $r.'' | Should -Be 'blue' } } @@ -873,157 +877,158 @@ Describe 'Get-PodeUrl' { $WebEvent = @{ Endpoint = @{ Protocol = 'http' - Address = 'foo.com/'; + Address = 'foo.com/' } - Path = 'about' + Path = 'about' } - Get-PodeUrl | Should Be 'http://foo.com/about' + Get-PodeUrl | Should -Be 'http://foo.com/about' } } Describe 'Convert-PodePathPatternToRegex' { It 'Convert a path to regex' { - Convert-PodePathPatternToRegex -Path '/api*' | Should Be '^[\\/]api.*?$' + Convert-PodePathPatternToRegex -Path '/api*' | Should -Be '^[\\/]api.*?$' } It 'Convert a path to regex non-strict' { - Convert-PodePathPatternToRegex -Path '/api*' -NotStrict | Should Be '[\\/]api.*?' + Convert-PodePathPatternToRegex -Path '/api*' -NotStrict | Should -Be '[\\/]api.*?' } It 'Convert a path to regex, but not slashes' { - Convert-PodePathPatternToRegex -Path '/api*' -NotSlashes | Should Be '^/api.*?$' + Convert-PodePathPatternToRegex -Path '/api*' -NotSlashes | Should -Be '^/api.*?$' } It 'Convert a path to regex, but not slashes and non-strict' { - Convert-PodePathPatternToRegex -Path '/api*' -NotSlashes -NotStrict | Should Be '/api.*?' + Convert-PodePathPatternToRegex -Path '/api*' -NotSlashes -NotStrict | Should -Be '/api.*?' } It 'Convert file to regex' { - Convert-PodePathPatternToRegex -Path 'state.json' | Should Be '^state\.json$' + Convert-PodePathPatternToRegex -Path 'state.json' | Should -Be '^state\.json$' } It 'Convert file to regex non-strict' { - Convert-PodePathPatternToRegex -Path 'state.json' -NotStrict | Should Be 'state\.json' + Convert-PodePathPatternToRegex -Path 'state.json' -NotStrict | Should -Be 'state\.json' } It 'Convert empty to regex' { - Convert-PodePathPatternToRegex -Path '' | Should Be '^$' + Convert-PodePathPatternToRegex -Path '' | Should -Be '^$' } It 'Convert empty to regex non-strict' { - Convert-PodePathPatternToRegex -Path '' -NotStrict | Should Be '' + Convert-PodePathPatternToRegex -Path '' -NotStrict | Should -Be '' } It 'Convert extension wildcard to regex' { - Convert-PodePathPatternToRegex -Path 'state.*' | Should Be '^state\..*?$' + Convert-PodePathPatternToRegex -Path 'state.*' | Should -Be '^state\..*?$' } It 'Convert extension wildcard to regex non-strict' { - Convert-PodePathPatternToRegex -Path 'state.*' -NotStrict | Should Be 'state\..*?' + Convert-PodePathPatternToRegex -Path 'state.*' -NotStrict | Should -Be 'state\..*?' } It 'Convert filename wildcard to regex' { - Convert-PodePathPatternToRegex -Path '*.json' | Should Be '^.*?\.json$' + Convert-PodePathPatternToRegex -Path '*.json' | Should -Be '^.*?\.json$' } It 'Convert filename wildcard to regex non-strict' { - Convert-PodePathPatternToRegex -Path '*.json' -NotStrict | Should Be '.*?\.json' + Convert-PodePathPatternToRegex -Path '*.json' -NotStrict | Should -Be '.*?\.json' } It 'Convert double wildcard to regex' { - Convert-PodePathPatternToRegex -Path '*.*' | Should Be '^.*?\..*?$' + Convert-PodePathPatternToRegex -Path '*.*' | Should -Be '^.*?\..*?$' } It 'Convert double wildcard to regex non-strict' { - Convert-PodePathPatternToRegex -Path '*.*' -NotStrict | Should Be '.*?\..*?' + Convert-PodePathPatternToRegex -Path '*.*' -NotStrict | Should -Be '.*?\..*?' } } Describe 'Convert-PodePathPatternsToRegex' { It 'Convert paths to regex' { - Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') | Should Be '^([\\/]api.*?|[\\/]users.*?)$' + Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') | Should -Be '^([\\/]api.*?|[\\/]users.*?)$' } It 'Convert paths to regex non-strict' { - Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') -NotStrict | Should Be '([\\/]api.*?|[\\/]users.*?)' + Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') -NotStrict | Should -Be '([\\/]api.*?|[\\/]users.*?)' } It 'Convert paths to regex, but not slashes' { - Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') -NotSlashes | Should Be '^(/api.*?|/users.*?)$' + Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') -NotSlashes | Should -Be '^(/api.*?|/users.*?)$' } It 'Convert paths to regex, but not slashes and non-strict' { - Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') -NotSlashes -NotStrict | Should Be '(/api.*?|/users.*?)' + Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*') -NotSlashes -NotStrict | Should -Be '(/api.*?|/users.*?)' } It 'Convert paths to regex with empty' { - Convert-PodePathPatternsToRegex -Paths @('', '/api*', '/users*') | Should Be '^([\\/]api.*?|[\\/]users.*?)$' + Convert-PodePathPatternsToRegex -Paths @('', '/api*', '/users*') | Should -Be '^([\\/]api.*?|[\\/]users.*?)$' } It 'Convert paths to regex non-strict with empty' { - Convert-PodePathPatternsToRegex -Paths @('', '/api*', '/users*') -NotStrict | Should Be '([\\/]api.*?|[\\/]users.*?)' + Convert-PodePathPatternsToRegex -Paths @('', '/api*', '/users*') -NotStrict | Should -Be '([\\/]api.*?|[\\/]users.*?)' } It 'Convert paths to regex, but not slashes with empty' { - Convert-PodePathPatternsToRegex -Paths @('/api*', '', '/users*') -NotSlashes | Should Be '^(/api.*?|/users.*?)$' + Convert-PodePathPatternsToRegex -Paths @('/api*', '', '/users*') -NotSlashes | Should -Be '^(/api.*?|/users.*?)$' } It 'Convert paths to regex, but not slashes and non-strict with empty' { - Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*', '') -NotSlashes -NotStrict | Should Be '(/api.*?|/users.*?)' + Convert-PodePathPatternsToRegex -Paths @('/api*', '/users*', '') -NotSlashes -NotStrict | Should -Be '(/api.*?|/users.*?)' } It 'Convert empty to regex' { - Convert-PodePathPatternsToRegex -Paths @('') | Should Be $null + Convert-PodePathPatternsToRegex -Paths @('') | Should -Be $null } It 'Convert empty to regex' { - Convert-PodePathPatternsToRegex -Paths @('', '') | Should Be $null + Convert-PodePathPatternsToRegex -Paths @('', '') | Should -Be $null } It 'Convert extension wildcard to regex' { - Convert-PodePathPatternsToRegex -Paths @('state.*') | Should Be '^(state\..*?)$' + Convert-PodePathPatternsToRegex -Paths @('state.*') | Should -Be '^(state\..*?)$' } It 'Convert extension wildcard to regex non-strict' { - Convert-PodePathPatternsToRegex -Paths @('state.*') -NotStrict | Should Be '(state\..*?)' + Convert-PodePathPatternsToRegex -Paths @('state.*') -NotStrict | Should -Be '(state\..*?)' } It 'Convert filename wildcard to regex' { - Convert-PodePathPatternsToRegex -Paths @('*.json') | Should Be '^(.*?\.json)$' + Convert-PodePathPatternsToRegex -Paths @('*.json') | Should -Be '^(.*?\.json)$' } It 'Convert filename wildcard to regex non-strict' { - Convert-PodePathPatternsToRegex -Paths @('*.json') -NotStrict | Should Be '(.*?\.json)' + Convert-PodePathPatternsToRegex -Paths @('*.json') -NotStrict | Should -Be '(.*?\.json)' } It 'Convert double wildcard to regex' { - Convert-PodePathPatternsToRegex -Paths @('*.*') | Should Be '^(.*?\..*?)$' + Convert-PodePathPatternsToRegex -Paths @('*.*') | Should -Be '^(.*?\..*?)$' } It 'Convert double wildcard to regex non-strict' { - Convert-PodePathPatternsToRegex -Paths @('*.*') -NotStrict | Should Be '(.*?\..*?)' + Convert-PodePathPatternsToRegex -Paths @('*.*') -NotStrict | Should -Be '(.*?\..*?)' } } Describe 'ConvertFrom-PodeFile' { It 'Generates dynamic content' { $content = 'Value = $(1+1)' - ConvertFrom-PodeFile -Content $content | Should Be 'Value = 2' + ConvertFrom-PodeFile -Content $content | Should -Be 'Value = 2' } It 'Generates dynamic content, using parameters' { $content = 'Value = $($data["number"])' - ConvertFrom-PodeFile -Content $content -Data @{ 'number' = 3 } | Should Be 'Value = 3' + ConvertFrom-PodeFile -Content $content -Data @{ 'number' = 3 } | Should -Be 'Value = 3' } } Describe 'Get-PodeRelativePath' { - $PodeContext = @{ 'Server' = @{ 'Root' = 'c:/' } } + BeforeAll { + $PodeContext = @{ 'Server' = @{ 'Root' = 'c:/' } } - It 'Returns back a literal path' { - Get-PodeRelativePath -Path 'c:/path' | Should Be 'c:/path' - } + It 'Returns back a literal path' { + Get-PodeRelativePath -Path 'c:/path' | Should -Be 'c:/path' + } } It 'Returns path for literal path when resolving' { $PodeContext = @{ @@ -1032,11 +1037,11 @@ Describe 'Get-PodeRelativePath' { } } - Get-PodeRelativePath -Path $pwd.Path -Resolve -JoinRoot | Should Be $pwd.Path + Get-PodeRelativePath -Path $pwd.Path -Resolve -JoinRoot | Should -Be $pwd.Path } It 'Returns back a relative path' { - Get-PodeRelativePath -Path './path' | Should Be './path' + Get-PodeRelativePath -Path './path' | Should -Be './path' } It 'Returns path for a relative path when resolving' { @@ -1046,11 +1051,11 @@ Describe 'Get-PodeRelativePath' { } } - Get-PodeRelativePath -Path ".\src" -Resolve -JoinRoot | Should Be (Join-Path $pwd.Path "src") + Get-PodeRelativePath -Path '.\src' -Resolve -JoinRoot | Should -Be (Join-Path $pwd.Path 'src') } It 'Returns path for a relative path joined to default root' { - Get-PodeRelativePath -Path './path' -JoinRoot | Should Be 'c:/./path' + Get-PodeRelativePath -Path './path' -JoinRoot | Should -Be 'c:/./path' } It 'Returns resolved path for a relative path joined to default root when resolving' { @@ -1060,62 +1065,64 @@ Describe 'Get-PodeRelativePath' { } } - Get-PodeRelativePath -Path './src' -JoinRoot -Resolve | Should Be (Join-Path $pwd.Path "src") + Get-PodeRelativePath -Path './src' -JoinRoot -Resolve | Should -Be (Join-Path $pwd.Path 'src') } It 'Returns path for a relative path joined to passed root' { - Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should Be 'e:/./path' + Get-PodeRelativePath -Path './path' -JoinRoot -RootPath 'e:/' | Should -Be 'e:/./path' } It 'Throws error for path ot existing' { Mock Test-PodePath { return $false } - { Get-PodeRelativePath -Path './path' -TestPath } | Should Throw 'The path does not exist' + { Get-PodeRelativePath -Path './path' -TestPath } | Should -Throw -ExpectedMessage '*The path does not exist*' } } Describe 'Get-PodeWildcardFiles' { - Mock Get-PodeRelativePath { return $Path } - Mock Get-ChildItem { - $ext = [System.IO.Path]::GetExtension($Path) - return @(@{ 'FullName' = "./file1$($ext)" }) + BeforeAll { + Mock Get-PodeRelativePath { return $Path } + Mock Get-ChildItem { + $ext = [System.IO.Path]::GetExtension($Path) + return @(@{ 'FullName' = "./file1$($ext)" }) + } } It 'Get files after adding a wildcard to a directory' { $result = @(Get-PodeWildcardFiles -Path './path' -Wildcard '*.ps1') - $result.Length | Should Be 1 - $result[0] | Should Be './file1.ps1' + $result.Length | Should -Be 1 + $result[0] | Should -Be './file1.ps1' } It 'Get files for wildcard path' { $result = @(Get-PodeWildcardFiles -Path './path/*.png') - $result.Length | Should Be 1 - $result[0] | Should Be './file1.png' + $result.Length | Should -Be 1 + $result[0] | Should -Be './file1.png' } It 'Returns null for non-wildcard path' { - Get-PodeWildcardFiles -Path './some/path/file.txt' | Should Be $null + Get-PodeWildcardFiles -Path './some/path/file.txt' | Should -Be $null } } Describe 'Test-PodeIsServerless' { It 'Returns true' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } - Test-PodeIsServerless | Should Be $true + Test-PodeIsServerless | Should -Be $true } It 'Returns false' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $false } } - Test-PodeIsServerless | Should Be $false + Test-PodeIsServerless | Should -Be $false } It 'Throws error if serverless' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } - { Test-PodeIsServerless -ThrowError } | Should Throw 'not supported in a serverless' + { Test-PodeIsServerless -ThrowError } | Should -Throw -ExpectedMessage '*not supported in a serverless*' } It 'Throws no error if not serverless' { $PodeContext = @{ 'Server' = @{ 'IsServerless' = $false } } - { Test-PodeIsServerless -ThrowError } | Should Not Throw 'not supported in a serverless' + { Test-PodeIsServerless -ThrowError } | Should -Not -Throw -ExpectedMessage '*not supported in a serverless*' } } @@ -1127,11 +1134,12 @@ Describe 'Close-PodeRunspaces' { } Describe 'Close-PodeServerInternal' { - Mock Close-PodeRunspaces { } - Mock Stop-PodeFileMonitor { } - Mock Close-PodeDisposable { } - Mock Remove-PodePSDrives { } - Mock Write-Host { } + BeforeAll { + Mock Close-PodeRunspaces { } + Mock Stop-PodeFileMonitor { } + Mock Close-PodeDisposable { } + Mock Remove-PodePSDrives { } + Mock Write-Host { } } It 'Closes out pode, but with no done flag' { $PodeContext = @{ 'Server' = @{ 'Types' = 'Server' } } @@ -1155,54 +1163,55 @@ Describe 'Close-PodeServerInternal' { Describe 'Get-PodeEndpointUrl' { It 'Returns default endpoint url' { $PodeContext = @{ Server = @{ - Endpoints = @{ - Example1 = @{ - Port = 6000 - Address = '127.0.0.1' - FriendlyName = 'thing.com' - Hostname = 'thing.com' - Protocol = 'https' + Endpoints = @{ + Example1 = @{ + Port = 6000 + Address = '127.0.0.1' + FriendlyName = 'thing.com' + Hostname = 'thing.com' + Protocol = 'https' + } } } - } } + } - Get-PodeEndpointUrl | Should Be 'https://thing.com:6000' + Get-PodeEndpointUrl | Should -Be 'https://thing.com:6000' } It 'Returns a passed endpoint url' { $endpoint = @{ - Port = 7000 - Address = '127.0.0.1' + Port = 7000 + Address = '127.0.0.1' FriendlyName = 'stuff.com' - Hostname = 'stuff.com' - Protocol = 'http' + Hostname = 'stuff.com' + Protocol = 'http' } - Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'http://stuff.com:7000' + Get-PodeEndpointUrl -Endpoint $endpoint | Should -Be 'http://stuff.com:7000' } It 'Returns a passed endpoint url, with default port for http' { $endpoint = @{ - Port = 8080 - Address = '127.0.0.1' + Port = 8080 + Address = '127.0.0.1' FriendlyName = 'stuff.com' - Hostname = 'stuff.com' - Protocol = 'http' + Hostname = 'stuff.com' + Protocol = 'http' } - Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'http://stuff.com:8080' + Get-PodeEndpointUrl -Endpoint $endpoint | Should -Be 'http://stuff.com:8080' } It 'Returns a passed endpoint url, with default port for https' { $endpoint = @{ - Port = 8443 - Address = '127.0.0.1' + Port = 8443 + Address = '127.0.0.1' FriendlyName = 'stuff.com' - Hostname = 'stuff.com' - Protocol = 'https' + Hostname = 'stuff.com' + Protocol = 'https' } - Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'https://stuff.com:8443' + Get-PodeEndpointUrl -Endpoint $endpoint | Should -Be 'https://stuff.com:8443' } It 'Returns a passed endpoint url, using raw url' { @@ -1210,131 +1219,132 @@ Describe 'Get-PodeEndpointUrl' { Url = 'https://stuff.com:8443' } - Get-PodeEndpointUrl -Endpoint $endpoint | Should Be 'https://stuff.com:8443' + Get-PodeEndpointUrl -Endpoint $endpoint | Should -Be 'https://stuff.com:8443' } } Describe 'Get-PodeCount' { Context 'Null' { - It 'Null value'{ - Get-PodeCount $null | Should Be 0 + It 'Null value' { + Get-PodeCount $null | Should -Be 0 } } - Context 'String'{ + Context 'String' { It 'Empty' { - Get-PodeCount '' | Should Be 0 + Get-PodeCount '' | Should -Be 0 } It 'Whitespace' { - Get-PodeCount ' ' | Should Be 1 - Get-PodeCount ' ' | Should Be 3 + Get-PodeCount ' ' | Should -Be 1 + Get-PodeCount ' ' | Should -Be 3 } } - Context 'Numbers'{ - It 'Number'{ - Get-PodeCount 2 | Should Be 1 + Context 'Numbers' { + It 'Number' { + Get-PodeCount 2 | Should -Be 1 } } Context 'Array' { - It 'Empty'{ - Get-PodeCount @() | Should Be 0 + It 'Empty' { + Get-PodeCount @() | Should -Be 0 } - It 'One'{ - Get-PodeCount @(4) | Should Be 1 - Get-PodeCount @('data') | Should Be 1 - Get-PodeCount @(@(3)) | Should Be 1 - Get-PodeCount @(@{}) | Should Be 1 + It 'One' { + Get-PodeCount @(4) | Should -Be 1 + Get-PodeCount @('data') | Should -Be 1 + Get-PodeCount @(@(3)) | Should -Be 1 + Get-PodeCount @(@{}) | Should -Be 1 } - It 'Two'{ - Get-PodeCount @(4, 7) | Should Be 2 - Get-PodeCount @('data', 9) | Should Be 2 - Get-PodeCount @(@(3), @()) | Should Be 2 - Get-PodeCount @(@{}, @{}) | Should Be 2 + It 'Two' { + Get-PodeCount @(4, 7) | Should -Be 2 + Get-PodeCount @('data', 9) | Should -Be 2 + Get-PodeCount @(@(3), @()) | Should -Be 2 + Get-PodeCount @(@{}, @{}) | Should -Be 2 } } Context 'Hashtable' { - It 'Empty'{ - Get-PodeCount @{} | Should Be 0 + It 'Empty' { + Get-PodeCount @{} | Should -Be 0 } - It 'One'{ - Get-PodeCount @{'testElement1'=4} | Should Be 1 - Get-PodeCount @{'testElement1'='test'} | Should Be 1 - Get-PodeCount @{'testElement1'=@()} | Should Be 1 - Get-PodeCount @{'testElement1'=@{"insideElement"="won't count"}} | Should Be 1 + It 'One' { + Get-PodeCount @{'testElement1' = 4 } | Should -Be 1 + Get-PodeCount @{'testElement1' = 'test' } | Should -Be 1 + Get-PodeCount @{'testElement1' = @() } | Should -Be 1 + Get-PodeCount @{'testElement1' = @{'insideElement' = "won't count" } } | Should -Be 1 } - It 'Two'{ - Get-PodeCount @{'testElement1'=4; 'testElement2'=10} | Should Be 2 - Get-PodeCount @{'testElement1'='test'; 'testElement2'=10} | Should Be 2 - Get-PodeCount @{'testElement1'=@(); 'testElement2'=@(9)} | Should Be 2 - Get-PodeCount @{'testElement1'=@{"insideElement"="won't count"}; 'testElement2'=@('testing')} | Should Be 2 + It 'Two' { + Get-PodeCount @{'testElement1' = 4; 'testElement2' = 10 } | Should -Be 2 + Get-PodeCount @{'testElement1' = 'test'; 'testElement2' = 10 } | Should -Be 2 + Get-PodeCount @{'testElement1' = @(); 'testElement2' = @(9) } | Should -Be 2 + Get-PodeCount @{'testElement1' = @{'insideElement' = "won't count" }; 'testElement2' = @('testing') } | Should -Be 2 } } } Describe 'Convert-PodePathSeparators' { Context 'Null' { - It 'Null'{ - Convert-PodePathSeparators -Path $null | Should Be $null + It 'Null' { + Convert-PodePathSeparators -Path $null | Should -Be $null } } Context 'String' { It 'Empty' { - Convert-PodePathSeparators -Path '' | Should Be $null - Convert-PodePathSeparators -Path ' ' | Should Be $null + Convert-PodePathSeparators -Path '' | Should -Be $null + Convert-PodePathSeparators -Path ' ' | Should -Be $null } It 'Value' { - Convert-PodePathSeparators -Path 'anyValue' | Should Be 'anyValue' - Convert-PodePathSeparators -Path 1 | Should Be 1 + Convert-PodePathSeparators -Path 'anyValue' | Should -Be 'anyValue' + Convert-PodePathSeparators -Path 1 | Should -Be 1 } It 'Path' { - Convert-PodePathSeparators -Path 'one/Seperators' | Should Be "one$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one\Seperators' | Should Be "one$([System.IO.Path]::DirectorySeparatorChar)Seperators" + Convert-PodePathSeparators -Path 'one/Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)Seperators" + Convert-PodePathSeparators -Path 'one\Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one/two/Seperators' | Should Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one\two\Seperators' | Should Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one/two\Seperators' | Should Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" - Convert-PodePathSeparators -Path 'one\two/Seperators' | Should Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" + Convert-PodePathSeparators -Path 'one/two/Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" + Convert-PodePathSeparators -Path 'one\two\Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" + Convert-PodePathSeparators -Path 'one/two\Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" + Convert-PodePathSeparators -Path 'one\two/Seperators' | Should -Be "one$([System.IO.Path]::DirectorySeparatorChar)two$([System.IO.Path]::DirectorySeparatorChar)Seperators" } } - Context 'Array'{ - It 'Null'{ - Convert-PodePathSeparators -Path @($null) | Should Be $null - Convert-PodePathSeparators -Path @($null, $null) | Should Be $null + Context 'Array' { + It 'Null' { + Convert-PodePathSeparators -Path @($null) | Should -Be $null + Convert-PodePathSeparators -Path @($null, $null) | Should -Be $null } It 'Single' { - Convert-PodePathSeparators -Path @('noSeperators') | Should Be @('noSeperators') - Convert-PodePathSeparators -Path @('some/Seperators') | Should Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators") - Convert-PodePathSeparators -Path @('some\Seperators') | Should Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators") + Convert-PodePathSeparators -Path @('noSeperators') | Should -Be @('noSeperators') + Convert-PodePathSeparators -Path @('some/Seperators') | Should -Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators") + Convert-PodePathSeparators -Path @('some\Seperators') | Should -Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators") - Convert-PodePathSeparators -Path @('') | Should Be $null - Convert-PodePathSeparators -Path @(' ') | Should Be $null + Convert-PodePathSeparators -Path @('') | Should -Be $null + Convert-PodePathSeparators -Path @(' ') | Should -Be $null } It 'Double' { - Convert-PodePathSeparators -Path @('noSeperators1', 'noSeperators2') | Should Be @('noSeperators1', 'noSeperators2') - Convert-PodePathSeparators -Path @('some/Seperators', 'some\Seperators') | Should Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators", "some$([System.IO.Path]::DirectorySeparatorChar)Seperators") + Convert-PodePathSeparators -Path @('noSeperators1', 'noSeperators2') | Should -Be @('noSeperators1', 'noSeperators2') + Convert-PodePathSeparators -Path @('some/Seperators', 'some\Seperators') | Should -Be @("some$([System.IO.Path]::DirectorySeparatorChar)Seperators", "some$([System.IO.Path]::DirectorySeparatorChar)Seperators") - Convert-PodePathSeparators -Path @('', ' ') | Should Be $null - Convert-PodePathSeparators -Path @(' ', '') | Should Be $null + Convert-PodePathSeparators -Path @('', ' ') | Should -Be $null + Convert-PodePathSeparators -Path @(' ', '') | Should -Be $null } } } Describe 'Out-PodeHost' { - Mock Out-Default {} - + BeforeAll { + Mock Out-Default {} + } It 'Writes a message to the Host by parameters' { Out-PodeHost -InputObject 'Hello' Assert-MockCalled Out-Default -Scope It -Times 1 @@ -1363,207 +1373,208 @@ Describe 'Remove-PodeNullKeysFromHashtable' { $ht | Remove-PodeNullKeysFromHashtable - $ht.ContainsKey('Value1') | Should Be $false - $ht.ContainsKey('Value2') | Should Be $true - $ht.Value2.ContainsKey('Value3') | Should Be $true - $ht.Value2.ContainsKey('Value4') | Should Be $false + $ht.ContainsKey('Value1') | Should -Be $false + $ht.ContainsKey('Value2') | Should -Be $true + $ht.Value2.ContainsKey('Value3') | Should -Be $true + $ht.Value2.ContainsKey('Value4') | Should -Be $false } } Describe 'Get-PodeDefaultPort' { It 'Returns default port for http' { - Get-PodeDefaultPort -Protocol Http | Should Be 8080 + Get-PodeDefaultPort -Protocol Http | Should -Be 8080 } It 'Returns default port for https' { - Get-PodeDefaultPort -Protocol Https | Should Be 8443 + Get-PodeDefaultPort -Protocol Https | Should -Be 8443 } It 'Returns default port for smtp' { - Get-PodeDefaultPort -Protocol Smtp | Should Be 25 + Get-PodeDefaultPort -Protocol Smtp | Should -Be 25 } It 'Returns default port for smtps - implicit' { - Get-PodeDefaultPort -Protocol Smtps -TlsMode Implicit | Should Be 465 + Get-PodeDefaultPort -Protocol Smtps -TlsMode Implicit | Should -Be 465 } It 'Returns default port for smtps - explicit' { - Get-PodeDefaultPort -Protocol Smtps -TlsMode Explicit | Should Be 587 + Get-PodeDefaultPort -Protocol Smtps -TlsMode Explicit | Should -Be 587 } It 'Returns default port for tcp' { - Get-PodeDefaultPort -Protocol Tcp | Should Be 9001 + Get-PodeDefaultPort -Protocol Tcp | Should -Be 9001 } It 'Returns default port for ws' { - Get-PodeDefaultPort -Protocol Ws | Should Be 9080 + Get-PodeDefaultPort -Protocol Ws | Should -Be 9080 } It 'Returns default port for wss' { - Get-PodeDefaultPort -Protocol Wss | Should Be 9443 + Get-PodeDefaultPort -Protocol Wss | Should -Be 9443 } } Describe 'Convert-PodeQueryStringToHashTable' { It 'Emty for no uri' { $result = Convert-PodeQueryStringToHashTable -Uri ([string]::Empty) - $result.Count | Should Be 0 + $result.Count | Should -Be 0 } It 'Emty for uri but no query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/users" - $result.Count | Should Be 0 + $result = Convert-PodeQueryStringToHashTable -Uri '/api/users' + $result.Count | Should -Be 0 } It 'Hashtable for root query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/?Name=Bob" - $result.Count | Should Be 1 - $result['Name'] | Should Be 'Bob' + $result = Convert-PodeQueryStringToHashTable -Uri '/?Name=Bob' + $result.Count | Should -Be 1 + $result['Name'] | Should -Be 'Bob' } It 'Hashtable for root query, no slash' { - $result = Convert-PodeQueryStringToHashTable -Uri "?Name=Bob" - $result.Count | Should Be 1 - $result['Name'] | Should Be 'Bob' + $result = Convert-PodeQueryStringToHashTable -Uri '?Name=Bob' + $result.Count | Should -Be 1 + $result['Name'] | Should -Be 'Bob' } It 'Hashtable for root multi-query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/?Name=Bob&Age=42" - $result.Count | Should Be 2 - $result['Name'] | Should Be 'Bob' - $result['Age'] | Should Be 42 + $result = Convert-PodeQueryStringToHashTable -Uri '/?Name=Bob&Age=42' + $result.Count | Should -Be 2 + $result['Name'] | Should -Be 'Bob' + $result['Age'] | Should -Be 42 } It 'Hashtable for root multi-query, no slash' { - $result = Convert-PodeQueryStringToHashTable -Uri "?Name=Bob&Age=42" - $result.Count | Should Be 2 - $result['Name'] | Should Be 'Bob' - $result['Age'] | Should Be 42 + $result = Convert-PodeQueryStringToHashTable -Uri '?Name=Bob&Age=42' + $result.Count | Should -Be 2 + $result['Name'] | Should -Be 'Bob' + $result['Age'] | Should -Be 42 } It 'Hashtable for non-root query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/user?Name=Bob" - $result.Count | Should Be 1 - $result['Name'] | Should Be 'Bob' + $result = Convert-PodeQueryStringToHashTable -Uri '/api/user?Name=Bob' + $result.Count | Should -Be 1 + $result['Name'] | Should -Be 'Bob' } It 'Hashtable for non-root multi-query' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/user?Name=Bob&Age=42" - $result.Count | Should Be 2 - $result['Name'] | Should Be 'Bob' - $result['Age'] | Should Be 42 + $result = Convert-PodeQueryStringToHashTable -Uri '/api/user?Name=Bob&Age=42' + $result.Count | Should -Be 2 + $result['Name'] | Should -Be 'Bob' + $result['Age'] | Should -Be 42 } It 'Hashtable for non-root multi-query, end slash' { - $result = Convert-PodeQueryStringToHashTable -Uri "/api/user/?Name=Bob&Age=42" - $result.Count | Should Be 2 - $result['Name'] | Should Be 'Bob' - $result['Age'] | Should Be 42 + $result = Convert-PodeQueryStringToHashTable -Uri '/api/user/?Name=Bob&Age=42' + $result.Count | Should -Be 2 + $result['Name'] | Should -Be 'Bob' + $result['Age'] | Should -Be 42 } } Describe 'ConvertFrom-PodeHeaderQValue' { It 'Returns empty' { $result = ConvertFrom-PodeHeaderQValue -Value '' - $result.Count | Should Be 0 + $result.Count | Should -Be 0 } It 'Returns values default to 1' { $result = ConvertFrom-PodeHeaderQValue -Value 'gzip,deflate' - $result.Count | Should Be 2 + $result.Count | Should -Be 2 - $result['gzip'] | Should Be 1.0 - $result['deflate'] | Should Be 1.0 + $result['gzip'] | Should -Be 1.0 + $result['deflate'] | Should -Be 1.0 } It 'Returns values with set quality' { $result = ConvertFrom-PodeHeaderQValue -Value 'gzip;q=0.1,deflate;q=0.8' - $result.Count | Should Be 2 + $result.Count | Should -Be 2 - $result['gzip'] | Should Be 0.1 - $result['deflate'] | Should Be 0.8 + $result['gzip'] | Should -Be 0.1 + $result['deflate'] | Should -Be 0.8 } It 'Returns values with mix' { $result = ConvertFrom-PodeHeaderQValue -Value 'gzip,deflate;q=0.8,identity;q=0' - $result.Count | Should Be 3 + $result.Count | Should -Be 3 - $result['gzip'] | Should Be 1.0 - $result['deflate'] | Should Be 0.8 - $result['identity'] | Should Be 0 + $result['gzip'] | Should -Be 1.0 + $result['deflate'] | Should -Be 0.8 + $result['identity'] | Should -Be 0 } } Describe 'Get-PodeAcceptEncoding' { - $PodeContext = @{ - Server = @{ - Web = @{ Compression = @{ Enabled = $true } } - Compression = @{ Encodings = @('gzip', 'deflate', 'x-gzip') } - } - } + BeforeEach { + $PodeContext = @{ + Server = @{ + Web = @{ Compression = @{ Enabled = $true } } + Compression = @{ Encodings = @('gzip', 'deflate', 'x-gzip') } + } + } } It 'Returns empty for no encoding' { - Get-PodeAcceptEncoding -AcceptEncoding '' | Should Be '' + Get-PodeAcceptEncoding -AcceptEncoding '' | Should -Be '' } It 'Returns empty when disabled' { $PodeContext.Server.Web.Compression.Enabled = $false - Get-PodeAcceptEncoding -AcceptEncoding '' | Should Be '' + Get-PodeAcceptEncoding -AcceptEncoding '' | Should -Be '' } It 'Returns first encoding for all default' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'gzip,deflate' | Should Be 'gzip' + Get-PodeAcceptEncoding -AcceptEncoding 'gzip,deflate' | Should -Be 'gzip' } It 'Returns gzip for older x-gzip' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'x-gzip' | Should Be 'gzip' + Get-PodeAcceptEncoding -AcceptEncoding 'x-gzip' | Should -Be 'gzip' } It 'Returns empty if no encoding matches' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'br,compress' | Should Be '' + Get-PodeAcceptEncoding -AcceptEncoding 'br,compress' | Should -Be '' } It 'Returns empty if no encoding matches, and 1 encoding is disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'br,compress,gzip;q=0' | Should Be '' + Get-PodeAcceptEncoding -AcceptEncoding 'br,compress,gzip;q=0' | Should -Be '' } It 'Returns encoding when no other encoding matches, and 1 encoding matches' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'br,compress,gzip' | Should Be 'gzip' + Get-PodeAcceptEncoding -AcceptEncoding 'br,compress,gzip' | Should -Be 'gzip' } It 'Returns highest encoding when weighted' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'gzip;q=0.1,deflate' | Should Be 'deflate' + Get-PodeAcceptEncoding -AcceptEncoding 'gzip;q=0.1,deflate' | Should -Be 'deflate' } It 'Returns highest encoding when weighted, and identity disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'gzip;q=0.1,deflate,identity;q=0' | Should Be 'deflate' + Get-PodeAcceptEncoding -AcceptEncoding 'gzip;q=0.1,deflate,identity;q=0' | Should -Be 'deflate' } It 'Returns encoding even when none match, and identity disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' | Should Be '' + Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' | Should -Be '' } It 'Errors when no encoding matches, and identity disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - { Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' -ThrowError } | Should Throw 'HttpRequestException' + { Get-PodeAcceptEncoding -AcceptEncoding 'br,identity;q=0' -ThrowError } | Should -Throw -ExpectedMessage '*HttpRequestException*' } It 'Errors when no encoding matches, and wildcard disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - { Get-PodeAcceptEncoding -AcceptEncoding 'br,*;q=0' -ThrowError } | Should Throw 'HttpRequestException' + { Get-PodeAcceptEncoding -AcceptEncoding 'br,*;q=0' -ThrowError } | Should -Throw -ExpectedMessage '*HttpRequestException*' } It 'Returns empty if identity is allowed, but wildcard disabled' { $PodeContext.Server.Web.Compression.Enabled = $true - Get-PodeAcceptEncoding -AcceptEncoding 'identity,*;q=0' | Should Be '' + Get-PodeAcceptEncoding -AcceptEncoding 'identity,*;q=0' | Should -Be '' } } @@ -1575,131 +1586,131 @@ Describe 'Get-PodeTransferEncoding' { } It 'Returns empty for no encoding' { - Get-PodeTransferEncoding -TransferEncoding '' | Should Be '' + Get-PodeTransferEncoding -TransferEncoding '' | Should -Be '' } It 'Returns empty when just chunked' { - Get-PodeTransferEncoding -TransferEncoding 'chunked' | Should Be '' + Get-PodeTransferEncoding -TransferEncoding 'chunked' | Should -Be '' } It 'Returns first encoding that matches' { - Get-PodeTransferEncoding -TransferEncoding 'gzip,deflate' | Should Be 'gzip' + Get-PodeTransferEncoding -TransferEncoding 'gzip,deflate' | Should -Be 'gzip' } It 'Returns encoding when chunked' { - Get-PodeTransferEncoding -TransferEncoding 'gzip,chunked' | Should Be 'gzip' - Get-PodeTransferEncoding -TransferEncoding 'chunked,gzip' | Should Be 'gzip' + Get-PodeTransferEncoding -TransferEncoding 'gzip,chunked' | Should -Be 'gzip' + Get-PodeTransferEncoding -TransferEncoding 'chunked,gzip' | Should -Be 'gzip' } It 'Returns first invalid encoding when none match' { - Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' | Should Be 'compress' + Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' | Should -Be 'compress' } It 'Errors when no encoding matches' { - { Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' -ThrowError } | Should Throw 'HttpRequestException' + { Get-PodeTransferEncoding -TransferEncoding 'compress,chunked' -ThrowError } | Should -Throw -ExpectedMessage '*HttpRequestException*' } } Describe 'Get-PodeEncodingFromContentType' { It 'Return utf8 for no type' { $enc = Get-PodeEncodingFromContentType -ContentType '' - $enc.EncodingName | Should Be 'Unicode (UTF-8)' + $enc.EncodingName | Should -Be 'Unicode (UTF-8)' } It 'Return utf8 for no charset in type' { $enc = Get-PodeEncodingFromContentType -ContentType 'application/json' - $enc.EncodingName | Should Be 'Unicode (UTF-8)' + $enc.EncodingName | Should -Be 'Unicode (UTF-8)' } It 'Return ascii when charset is set' { $enc = Get-PodeEncodingFromContentType -ContentType 'application/json;charset=ascii' - $enc.EncodingName | Should Be 'US-ASCII' + $enc.EncodingName | Should -Be 'US-ASCII' } It 'Return utf8 when charset is set' { $enc = Get-PodeEncodingFromContentType -ContentType 'application/json;charset=utf-8' - $enc.EncodingName | Should Be 'Unicode (UTF-8)' + $enc.EncodingName | Should -Be 'Unicode (UTF-8)' } } Describe 'New-PodeCron' { It 'Returns a minutely expression' { - New-PodeCron -Every Minute | Should Be '* * * * *' + New-PodeCron -Every Minute | Should -Be '* * * * *' } It 'Returns an hourly expression' { - New-PodeCron -Every Hour | Should Be '0 * * * *' + New-PodeCron -Every Hour | Should -Be '0 * * * *' } It 'Returns a daily expression (by day)' { - New-PodeCron -Every Day | Should Be '0 0 * * *' + New-PodeCron -Every Day | Should -Be '0 0 * * *' } It 'Returns a daily expression (by date)' { - New-PodeCron -Every Date | Should Be '0 0 * * *' + New-PodeCron -Every Date | Should -Be '0 0 * * *' } It 'Returns a monthly expression' { - New-PodeCron -Every Month | Should Be '0 0 1 * *' + New-PodeCron -Every Month | Should -Be '0 0 1 * *' } It 'Returns a quarterly expression' { - New-PodeCron -Every Quarter | Should Be '0 0 1 1,4,7,10 *' + New-PodeCron -Every Quarter | Should -Be '0 0 1 1,4,7,10 *' } It 'Returns a yearly expression' { - New-PodeCron -Every Year | Should Be '0 0 1 1 *' + New-PodeCron -Every Year | Should -Be '0 0 1 1 *' } It 'Returns an expression for every 15mins' { - New-PodeCron -Every Minute -Interval 15 | Should Be '*/15 * * * *' + New-PodeCron -Every Minute -Interval 15 | Should -Be '*/15 * * * *' } It 'Returns an expression for every tues/fri at 1am' { - New-PodeCron -Every Day -Day Tuesday, Friday -Hour 1 | Should Be '0 1 * * 2,5' + New-PodeCron -Every Day -Day Tuesday, Friday -Hour 1 | Should -Be '0 1 * * 2,5' } It 'Returns an expression for every 15th of the month' { - New-PodeCron -Every Month -Date 15 | Should Be '0 0 15 * *' + New-PodeCron -Every Month -Date 15 | Should -Be '0 0 15 * *' } It 'Returns an expression for every other day, from the 2nd' { - New-PodeCron -Every Date -Interval 2 -Date 2 | Should Be '0 0 2/2 * *' + New-PodeCron -Every Date -Interval 2 -Date 2 | Should -Be '0 0 2/2 * *' } It 'Returns an expression for every june 1st' { - New-PodeCron -Every Year -Month June | Should Be '0 0 1 6 *' + New-PodeCron -Every Year -Month June | Should -Be '0 0 1 6 *' } It 'Returns an expression for every 15mins between 1am-5am' { - New-PodeCron -Every Minute -Interval 15 -Hour 1, 2, 3, 4, 5 | Should Be '*/15 1,2,3,4,5 * * *' + New-PodeCron -Every Minute -Interval 15 -Hour 1, 2, 3, 4, 5 | Should -Be '*/15 1,2,3,4,5 * * *' } It 'Returns an expression for every hour of every monday' { - New-PodeCron -Every Hour -Day Monday | Should Be '0 * * * 1' + New-PodeCron -Every Hour -Day Monday | Should -Be '0 * * * 1' } It 'Returns an expression for everyday at 5:15am' { - New-PodeCron -Every Day -Hour 5 -Minute 15 | Should Be '15 5 * * *' + New-PodeCron -Every Day -Hour 5 -Minute 15 | Should -Be '15 5 * * *' } It 'Throws an error for multiple Hours when using Interval' { - { New-PodeCron -Every Hour -Hour 2, 4 -Interval 3 } | Should Throw 'only supply a single' + { New-PodeCron -Every Hour -Hour 2, 4 -Interval 3 } | Should -Throw -ExpectedMessage '*only supply a single*' } It 'Throws an error for multiple Minutes when using Interval' { - { New-PodeCron -Every Minute -Minute 2, 4 -Interval 15 } | Should Throw 'only supply a single' + { New-PodeCron -Every Minute -Minute 2, 4 -Interval 15 } | Should -Throw -ExpectedMessage '*only supply a single*' } It 'Throws an error when using Interval without Every' { - { New-PodeCron -Interval 3 } | Should Throw 'Cannot supply an interval' + { New-PodeCron -Interval 3 } | Should -Throw -ExpectedMessage '*Cannot supply an interval*' } It 'Throws an error when using Interval for Every Quarter' { - { New-PodeCron -Every Quarter -Interval 3 } | Should Throw 'Cannot supply interval value for every quarter' + { New-PodeCron -Every Quarter -Interval 3 } | Should -Throw -ExpectedMessage 'Cannot supply interval value for every quarter' } It 'Throws an error when using Interval for Every Year' { - { New-PodeCron -Every Year -Interval 3 } | Should Throw 'Cannot supply interval value for every year' + { New-PodeCron -Every Year -Interval 3 } | Should -Throw -ExpectedMessage 'Cannot supply interval value for every year' } } \ No newline at end of file diff --git a/tests/unit/Logging.Tests.ps1 b/tests/unit/Logging.Tests.ps1 index 04856ac41..3d54a6a2a 100644 --- a/tests/unit/Logging.Tests.ps1 +++ b/tests/unit/Logging.Tests.ps1 @@ -1,26 +1,29 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Get-PodeLogger' { It 'Returns null as the logger does not exist' { $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Types' = @{}; } }; } - Get-PodeLogger -Name 'test' | Should Be $null + Get-PodeLogger -Name 'test' | Should -Be $null } It 'Returns terminal logger for name' { $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Types' = @{ 'test' = $null }; } }; } $result = (Get-PodeLogger -Name 'test') - $result | Should Be $null + $result | Should -Be $null } It 'Returns custom logger for name' { $PodeContext = @{ 'Server' = @{ 'Logging' = @{ 'Types' = @{ 'test' = { Write-Host 'hello' } }; } }; } $result = (Get-PodeLogger -Name 'test') - $result | Should Not Be $null - $result.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $result | Should -Not -Be $null + $result.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } } @@ -31,7 +34,7 @@ Describe 'Write-PodeLog' { Write-PodeLog -Name 'test' -InputObject 'test' - $PodeContext.LogsToProcess.Count | Should Be 0 + $PodeContext.LogsToProcess.Count | Should -Be 0 } It 'Adds a log item' { @@ -40,9 +43,9 @@ Describe 'Write-PodeLog' { Write-PodeLog -Name 'test' -InputObject 'test' - $PodeContext.LogsToProcess.Count | Should Be 1 - $PodeContext.LogsToProcess[0].Name | Should Be 'test' - $PodeContext.LogsToProcess[0].Item | Should Be 'test' + $PodeContext.LogsToProcess.Count | Should -Be 1 + $PodeContext.LogsToProcess[0].Name | Should -Be 'test' + $PodeContext.LogsToProcess[0].Item | Should -Be 'test' } } @@ -53,14 +56,15 @@ Describe 'Write-PodeErrorLog' { Write-PodeLog -Name 'test' -InputObject 'test' - $PodeContext.LogsToProcess.Count | Should Be 0 + $PodeContext.LogsToProcess.Count | Should -Be 0 } It 'Adds an error log item' { Mock Test-PodeLoggerEnabled { return $true } Mock Get-PodeLogger { return @{ Arguments = @{ - Levels = @('Error') - } } } + Levels = @('Error') + } + } } $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } @@ -69,96 +73,114 @@ Describe 'Write-PodeErrorLog' { Write-PodeErrorLog -ErrorRecord $Error[0] } - $PodeContext.LogsToProcess.Count | Should Be 1 - $PodeContext.LogsToProcess[0].Item.Message | Should Be 'some error' + $PodeContext.LogsToProcess.Count | Should -Be 1 + $PodeContext.LogsToProcess[0].Item.Message | Should -Be 'some error' } It 'Adds an exception log item' { Mock Test-PodeLoggerEnabled { return $true } Mock Get-PodeLogger { return @{ Arguments = @{ - Levels = @('Error') - } } } + Levels = @('Error') + } + } } $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } $exp = [exception]::new('some error') Write-PodeErrorLog -Exception $exp - $PodeContext.LogsToProcess.Count | Should Be 1 - $PodeContext.LogsToProcess[0].Item.Message | Should Be 'some error' + $PodeContext.LogsToProcess.Count | Should -Be 1 + $PodeContext.LogsToProcess[0].Item.Message | Should -Be 'some error' } It 'Does not log as Verbose not allowed' { Mock Test-PodeLoggerEnabled { return $true } Mock Get-PodeLogger { return @{ Arguments = @{ - Levels = @('Error') - } } } + Levels = @('Error') + } + } } $PodeContext = @{ LogsToProcess = New-Object System.Collections.ArrayList } $exp = [exception]::new('some error') Write-PodeErrorLog -Exception $exp -Level Verbose - $PodeContext.LogsToProcess.Count | Should Be 0 + $PodeContext.LogsToProcess.Count | Should -Be 0 } } Describe 'Get-PodeRequestLoggingName' { It 'Returns logger name' { - Get-PodeRequestLoggingName | Should Be '__pode_log_requests__' + Get-PodeRequestLoggingName | Should -Be '__pode_log_requests__' } } Describe 'Get-PodeErrorLoggingName' { It 'Returns logger name' { - Get-PodeErrorLoggingName | Should Be '__pode_log_errors__' + Get-PodeErrorLoggingName | Should -Be '__pode_log_errors__' } } Describe 'Protect-PodeLogItem' { - $item = 'Password=Hunter2, Email' - + BeforeEach { + $item = 'Password=Hunter2, Email' + } It 'Do nothing with no masks' { $PodeContext = @{ Server = @{ Logging = @{ Masking = @{ - Patterns = @() - }}}} + Patterns = @() + } + } + } + } - Protect-PodeLogItem -Item $item | Should Be $item + Protect-PodeLogItem -Item $item | Should -Be $item } It 'Mask whole item' { $PodeContext = @{ Server = @{ Logging = @{ Masking = @{ - Patterns = @('Password\=[a-z0-9]+') - Mask = '********' - }}}} + Patterns = @('Password\=[a-z0-9]+') + Mask = '********' + } + } + } + } - Protect-PodeLogItem -Item $item | Should Be '********, Email' + Protect-PodeLogItem -Item $item | Should -Be '********, Email' } It 'Mask item but keep before' { $PodeContext = @{ Server = @{ Logging = @{ Masking = @{ - Patterns = @('(?Password\=)[a-z0-9]+') - Mask = '********' - }}}} + Patterns = @('(?Password\=)[a-z0-9]+') + Mask = '********' + } + } + } + } - Protect-PodeLogItem -Item $item | Should Be 'Password=********, Email' + Protect-PodeLogItem -Item $item | Should -Be 'Password=********, Email' } It 'Mask item but keep after' { $PodeContext = @{ Server = @{ Logging = @{ Masking = @{ - Patterns = @('Password\=(?[a-z0-9]+)') - Mask = '********' - }}}} + Patterns = @('Password\=(?[a-z0-9]+)') + Mask = '********' + } + } + } + } - Protect-PodeLogItem -Item $item | Should Be '********Hunter2, Email' + Protect-PodeLogItem -Item $item | Should -Be '********Hunter2, Email' } It 'Mask item but keep before and after' { $PodeContext = @{ Server = @{ Logging = @{ Masking = @{ - Patterns = @('(?Password\=)(?[a-z0-9]+)') - Mask = '********' - }}}} + Patterns = @('(?Password\=)(?[a-z0-9]+)') + Mask = '********' + } + } + } + } - Protect-PodeLogItem -Item $item | Should Be 'Password=********Hunter2, Email' + Protect-PodeLogItem -Item $item | Should -Be 'Password=********Hunter2, Email' } } \ No newline at end of file diff --git a/tests/unit/Mappers.Tests.ps1 b/tests/unit/Mappers.Tests.ps1 index 7ace5c6c2..a8557fa3f 100644 --- a/tests/unit/Mappers.Tests.ps1 +++ b/tests/unit/Mappers.Tests.ps1 @@ -1,772 +1,1464 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Get-PodeContentType' { Context 'No extension supplied' { It 'Return the default type for empty' { - Get-PodeContentType -Extension ([string]::Empty) | Should Be 'text/plain' + Get-PodeContentType -Extension ([string]::Empty) | Should -Be 'text/plain' } It 'Return the default type for null' { - Get-PodeContentType -Extension $null | Should Be 'text/plain' + Get-PodeContentType -Extension $null | Should -Be 'text/plain' } It 'Return the default type for empty when DefaultIsNull' { - Get-PodeContentType -Extension ([string]::Empty) -DefaultIsNull | Should Be $null + Get-PodeContentType -Extension ([string]::Empty) -DefaultIsNull | Should -Be $null } It 'Return the default type for null when DefaultIsNull' { - Get-PodeContentType -Extension $null -DefaultIsNull | Should Be $null + Get-PodeContentType -Extension $null -DefaultIsNull | Should -Be $null } } Context 'Extension with no period' { It 'Add a period and return type' { - Get-PodeContentType -Extension 'mp3' | Should Be 'audio/mpeg' + Get-PodeContentType -Extension 'mp3' | Should -Be 'audio/mpeg' } It 'Add a period and return default' { - Get-PodeContentType -Extension '' | Should Be 'text/plain' + Get-PodeContentType -Extension '' | Should -Be 'text/plain' } } Context 'Extension with period' { It 'Add a period and return type' { - Get-PodeContentType -Extension '.mp3' | Should Be 'audio/mpeg' + Get-PodeContentType -Extension '.mp3' | Should -Be 'audio/mpeg' } It 'Add a period and return default' { - Get-PodeContentType -Extension '.' | Should Be 'text/plain' + Get-PodeContentType -Extension '.' | Should -Be 'text/plain' } } - Context 'All Extension Loop' { - $types = @{ - '.323' = 'text/h323'; - '.3g2' = 'video/3gpp2'; - '.3gp' = 'video/3gpp'; - '.3gp2' = 'video/3gpp2'; - '.3gpp' = 'video/3gpp'; - '.7z' = 'application/x-7z-compressed'; - '.aa' = 'audio/audible'; - '.aac' = 'audio/aac'; - '.aaf' = 'application/octet-stream'; - '.aax' = 'audio/vnd.audible.aax'; - '.ac3' = 'audio/ac3'; - '.aca' = 'application/octet-stream'; - '.accda' = 'application/msaccess.addin'; - '.accdb' = 'application/msaccess'; - '.accdc' = 'application/msaccess.cab'; - '.accde' = 'application/msaccess'; - '.accdr' = 'application/msaccess.runtime'; - '.accdt' = 'application/msaccess'; - '.accdw' = 'application/msaccess.webapplication'; - '.accft' = 'application/msaccess.ftemplate'; - '.acx' = 'application/internet-property-stream'; - '.addin' = 'text/xml'; - '.ade' = 'application/msaccess'; - '.adobebridge' = 'application/x-bridge-url'; - '.adp' = 'application/msaccess'; - '.adt' = 'audio/vnd.dlna.adts'; - '.adts' = 'audio/aac'; - '.afm' = 'application/octet-stream'; - '.ai' = 'application/postscript'; - '.aif' = 'audio/aiff'; - '.aifc' = 'audio/aiff'; - '.aiff' = 'audio/aiff'; - '.air' = 'application/vnd.adobe.air-application-installer-package+zip'; - '.amc' = 'application/mpeg'; - '.anx' = 'application/annodex'; - '.apk' = 'application/vnd.android.package-archive' ; - '.application' = 'application/x-ms-application'; - '.art' = 'image/x-jg'; - '.asa' = 'application/xml'; - '.asax' = 'application/xml'; - '.ascx' = 'application/xml'; - '.asd' = 'application/octet-stream'; - '.asf' = 'video/x-ms-asf'; - '.ashx' = 'application/xml'; - '.asi' = 'application/octet-stream'; - '.asm' = 'text/plain'; - '.asmx' = 'application/xml'; - '.aspx' = 'application/xml'; - '.asr' = 'video/x-ms-asf'; - '.asx' = 'video/x-ms-asf'; - '.atom' = 'application/atom+xml'; - '.au' = 'audio/basic'; - '.avi' = 'video/x-msvideo'; - '.axa' = 'audio/annodex'; - '.axs' = 'application/olescript'; - '.axv' = 'video/annodex'; - '.bas' = 'text/plain'; - '.bcpio' = 'application/x-bcpio'; - '.bin' = 'application/octet-stream'; - '.bmp' = 'image/bmp'; - '.c' = 'text/plain'; - '.cab' = 'application/octet-stream'; - '.caf' = 'audio/x-caf'; - '.calx' = 'application/vnd.ms-office.calx'; - '.cat' = 'application/vnd.ms-pki.seccat'; - '.cc' = 'text/plain'; - '.cd' = 'text/plain'; - '.cdda' = 'audio/aiff'; - '.cdf' = 'application/x-cdf'; - '.cer' = 'application/x-x509-ca-cert'; - '.cfg' = 'text/plain'; - '.chm' = 'application/octet-stream'; - '.class' = 'application/x-java-applet'; - '.clp' = 'application/x-msclip'; - '.cmd' = 'text/plain'; - '.cmx' = 'image/x-cmx'; - '.cnf' = 'text/plain'; - '.cod' = 'image/cis-cod'; - '.config' = 'application/xml'; - '.contact' = 'text/x-ms-contact'; - '.coverage' = 'application/xml'; - '.cpio' = 'application/x-cpio'; - '.cpp' = 'text/plain'; - '.crd' = 'application/x-mscardfile'; - '.crl' = 'application/pkix-crl'; - '.crt' = 'application/x-x509-ca-cert'; - '.cs' = 'text/plain'; - '.csdproj' = 'text/plain'; - '.csh' = 'application/x-csh'; - '.csproj' = 'text/plain'; - '.css' = 'text/css'; - '.csv' = 'text/csv'; - '.cur' = 'application/octet-stream'; - '.cxx' = 'text/plain'; - '.dat' = 'application/octet-stream'; - '.datasource' = 'application/xml'; - '.dbproj' = 'text/plain'; - '.dcr' = 'application/x-director'; - '.def' = 'text/plain'; - '.deploy' = 'application/octet-stream'; - '.der' = 'application/x-x509-ca-cert'; - '.dgml' = 'application/xml'; - '.dib' = 'image/bmp'; - '.dif' = 'video/x-dv'; - '.dir' = 'application/x-director'; - '.disco' = 'text/xml'; - '.divx' = 'video/divx'; - '.dll' = 'application/x-msdownload'; - '.dll.config' = 'text/xml'; - '.dlm' = 'text/dlm'; - '.doc' = 'application/msword'; - '.docm' = 'application/vnd.ms-word.document.macroEnabled.12'; - '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; - '.dot' = 'application/msword'; - '.dotm' = 'application/vnd.ms-word.template.macroEnabled.12'; - '.dotx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.template'; - '.dsp' = 'application/octet-stream'; - '.dsw' = 'text/plain'; - '.dtd' = 'text/xml'; - '.dtsconfig' = 'text/xml'; - '.dv' = 'video/x-dv'; - '.dvi' = 'application/x-dvi'; - '.dwf' = 'drawing/x-dwf'; - '.dwg' = 'application/acad'; - '.dwp' = 'application/octet-stream'; - '.dxf' = 'application/x-dxf' ; - '.dxr' = 'application/x-director'; - '.eml' = 'message/rfc822'; - '.emz' = 'application/octet-stream'; - '.eot' = 'application/vnd.ms-fontobject'; - '.eps' = 'application/postscript'; - '.etl' = 'application/etl'; - '.etx' = 'text/x-setext'; - '.evy' = 'application/envoy'; - '.exe' = 'application/octet-stream'; - '.exe.config' = 'text/xml'; - '.fdf' = 'application/vnd.fdf'; - '.fif' = 'application/fractals'; - '.filters' = 'application/xml'; - '.fla' = 'application/octet-stream'; - '.flac' = 'audio/flac'; - '.flr' = 'x-world/x-vrml'; - '.flv' = 'video/x-flv'; - '.fsscript' = 'application/fsharp-script'; - '.fsx' = 'application/fsharp-script'; - '.generictest' = 'application/xml'; - '.gif' = 'image/gif'; - '.gpx' = 'application/gpx+xml'; - '.group' = 'text/x-ms-group'; - '.gsm' = 'audio/x-gsm'; - '.gtar' = 'application/x-gtar'; - '.gz' = 'application/x-gzip'; - '.h' = 'text/plain'; - '.hdf' = 'application/x-hdf'; - '.hdml' = 'text/x-hdml'; - '.hhc' = 'application/x-oleobject'; - '.hhk' = 'application/octet-stream'; - '.hhp' = 'application/octet-stream'; - '.hlp' = 'application/winhlp'; - '.hpp' = 'text/plain'; - '.hqx' = 'application/mac-binhex40'; - '.hta' = 'application/hta'; - '.htc' = 'text/x-component'; - '.htm' = 'text/html'; - '.html' = 'text/html'; - '.htt' = 'text/webviewhtml'; - '.hxa' = 'application/xml'; - '.hxc' = 'application/xml'; - '.hxd' = 'application/octet-stream'; - '.hxe' = 'application/xml'; - '.hxf' = 'application/xml'; - '.hxh' = 'application/octet-stream'; - '.hxi' = 'application/octet-stream'; - '.hxk' = 'application/xml'; - '.hxq' = 'application/octet-stream'; - '.hxr' = 'application/octet-stream'; - '.hxs' = 'application/octet-stream'; - '.hxt' = 'text/html'; - '.hxv' = 'application/xml'; - '.hxw' = 'application/octet-stream'; - '.hxx' = 'text/plain'; - '.i' = 'text/plain'; - '.ico' = 'image/x-icon'; - '.ics' = 'application/octet-stream'; - '.idl' = 'text/plain'; - '.ief' = 'image/ief'; - '.iii' = 'application/x-iphone'; - '.inc' = 'text/plain'; - '.inf' = 'application/octet-stream'; - '.ini' = 'text/plain'; - '.inl' = 'text/plain'; - '.ins' = 'application/x-internet-signup'; - '.ipa' = 'application/x-itunes-ipa'; - '.ipg' = 'application/x-itunes-ipg'; - '.ipproj' = 'text/plain'; - '.ipsw' = 'application/x-itunes-ipsw'; - '.iqy' = 'text/x-ms-iqy'; - '.isp' = 'application/x-internet-signup'; - '.ite' = 'application/x-itunes-ite'; - '.itlp' = 'application/x-itunes-itlp'; - '.itms' = 'application/x-itunes-itms'; - '.itpc' = 'application/x-itunes-itpc'; - '.ivf' = 'video/x-ivf'; - '.jar' = 'application/java-archive'; - '.java' = 'application/octet-stream'; - '.jck' = 'application/liquidmotion'; - '.jcz' = 'application/liquidmotion'; - '.jfif' = 'image/pjpeg'; - '.jnlp' = 'application/x-java-jnlp-file'; - '.jpb' = 'application/octet-stream'; - '.jpe' = 'image/jpeg'; - '.jpeg' = 'image/jpeg'; - '.jpg' = 'image/jpeg'; - '.js' = 'application/javascript'; - '.json' = 'application/json'; - '.jsx' = 'text/jscript'; - '.jsxbin' = 'text/plain'; - '.latex' = 'application/x-latex'; - '.library-ms' = 'application/windows-library+xml'; - '.lit' = 'application/x-ms-reader'; - '.loadtest' = 'application/xml'; - '.lpk' = 'application/octet-stream'; - '.lsf' = 'video/x-la-asf'; - '.lst' = 'text/plain'; - '.lsx' = 'video/x-la-asf'; - '.lzh' = 'application/octet-stream'; - '.m13' = 'application/x-msmediaview'; - '.m14' = 'application/x-msmediaview'; - '.m1v' = 'video/mpeg'; - '.m2t' = 'video/vnd.dlna.mpeg-tts'; - '.m2ts' = 'video/vnd.dlna.mpeg-tts'; - '.m2v' = 'video/mpeg'; - '.m3u' = 'audio/x-mpegurl'; - '.m3u8' = 'audio/x-mpegurl'; - '.m4a' = 'audio/m4a'; - '.m4b' = 'audio/m4b'; - '.m4p' = 'audio/m4p'; - '.m4r' = 'audio/x-m4r'; - '.m4v' = 'video/x-m4v'; - '.mac' = 'image/x-macpaint'; - '.mak' = 'text/plain'; - '.man' = 'application/x-troff-man'; - '.manifest' = 'application/x-ms-manifest'; - '.map' = 'text/plain'; - '.markdown' = 'text/markdown'; - '.master' = 'application/xml'; - '.mbox' = 'application/mbox'; - '.md' = 'text/markdown'; - '.mda' = 'application/msaccess'; - '.mdb' = 'application/x-msaccess'; - '.mde' = 'application/msaccess'; - '.mdp' = 'application/octet-stream'; - '.me' = 'application/x-troff-me'; - '.mfp' = 'application/x-shockwave-flash'; - '.mht' = 'message/rfc822'; - '.mhtml' = 'message/rfc822'; - '.mid' = 'audio/mid'; - '.midi' = 'audio/mid'; - '.mix' = 'application/octet-stream'; - '.mk' = 'text/plain'; - '.mk3d' = 'video/x-matroska-3d'; - '.mka' = 'audio/x-matroska'; - '.mkv' = 'video/x-matroska'; - '.mmf' = 'application/x-smaf'; - '.mno' = 'text/xml'; - '.mny' = 'application/x-msmoney'; - '.mod' = 'video/mpeg'; - '.mov' = 'video/quicktime'; - '.movie' = 'video/x-sgi-movie'; - '.mp2' = 'video/mpeg'; - '.mp2v' = 'video/mpeg'; - '.mp3' = 'audio/mpeg'; - '.mp4' = 'video/mp4'; - '.mp4v' = 'video/mp4'; - '.mpa' = 'video/mpeg'; - '.mpe' = 'video/mpeg'; - '.mpeg' = 'video/mpeg'; - '.mpf' = 'application/vnd.ms-mediapackage'; - '.mpg' = 'video/mpeg'; - '.mpp' = 'application/vnd.ms-project'; - '.mpv2' = 'video/mpeg'; - '.mqv' = 'video/quicktime'; - '.ms' = 'application/x-troff-ms'; - '.msg' = 'application/vnd.ms-outlook'; - '.msi' = 'application/octet-stream'; - '.mso' = 'application/octet-stream'; - '.mts' = 'video/vnd.dlna.mpeg-tts'; - '.mtx' = 'application/xml'; - '.mvb' = 'application/x-msmediaview'; - '.mvc' = 'application/x-miva-compiled'; - '.mxp' = 'application/x-mmxp'; - '.nc' = 'application/x-netcdf'; - '.nsc' = 'video/x-ms-asf'; - '.nws' = 'message/rfc822'; - '.ocx' = 'application/octet-stream'; - '.oda' = 'application/oda'; - '.odb' = 'application/vnd.oasis.opendocument.database'; - '.odc' = 'application/vnd.oasis.opendocument.chart'; - '.odf' = 'application/vnd.oasis.opendocument.formula'; - '.odg' = 'application/vnd.oasis.opendocument.graphics'; - '.odh' = 'text/plain'; - '.odi' = 'application/vnd.oasis.opendocument.image'; - '.odl' = 'text/plain'; - '.odm' = 'application/vnd.oasis.opendocument.text-master'; - '.odp' = 'application/vnd.oasis.opendocument.presentation'; - '.ods' = 'application/vnd.oasis.opendocument.spreadsheet'; - '.odt' = 'application/vnd.oasis.opendocument.text'; - '.oga' = 'audio/ogg'; - '.ogg' = 'audio/ogg'; - '.ogv' = 'video/ogg'; - '.ogx' = 'application/ogg'; - '.one' = 'application/onenote'; - '.onea' = 'application/onenote'; - '.onepkg' = 'application/onenote'; - '.onetmp' = 'application/onenote'; - '.onetoc' = 'application/onenote'; - '.onetoc2' = 'application/onenote'; - '.opus' = 'audio/ogg'; - '.orderedtest' = 'application/xml'; - '.osdx' = 'application/opensearchdescription+xml'; - '.otf' = 'application/font-sfnt'; - '.otg' = 'application/vnd.oasis.opendocument.graphics-template'; - '.oth' = 'application/vnd.oasis.opendocument.text-web'; - '.otp' = 'application/vnd.oasis.opendocument.presentation-template'; - '.ots' = 'application/vnd.oasis.opendocument.spreadsheet-template'; - '.ott' = 'application/vnd.oasis.opendocument.text-template'; - '.oxt' = 'application/vnd.openofficeorg.extension'; - '.p10' = 'application/pkcs10'; - '.p12' = 'application/x-pkcs12'; - '.p7b' = 'application/x-pkcs7-certificates'; - '.p7c' = 'application/pkcs7-mime'; - '.p7m' = 'application/pkcs7-mime'; - '.p7r' = 'application/x-pkcs7-certreqresp'; - '.p7s' = 'application/pkcs7-signature'; - '.pbm' = 'image/x-portable-bitmap'; - '.pcast' = 'application/x-podcast'; - '.pct' = 'image/pict'; - '.pcx' = 'application/octet-stream'; - '.pcz' = 'application/octet-stream'; - '.pdf' = 'application/pdf'; - '.pfb' = 'application/octet-stream'; - '.pfm' = 'application/octet-stream'; - '.pfx' = 'application/x-pkcs12'; - '.pgm' = 'image/x-portable-graymap'; - '.pic' = 'image/pict'; - '.pict' = 'image/pict'; - '.pkgdef' = 'text/plain'; - '.pkgundef' = 'text/plain'; - '.pko' = 'application/vnd.ms-pki.pko'; - '.pls' = 'audio/scpls'; - '.pma' = 'application/x-perfmon'; - '.pmc' = 'application/x-perfmon'; - '.pml' = 'application/x-perfmon'; - '.pmr' = 'application/x-perfmon'; - '.pmw' = 'application/x-perfmon'; - '.png' = 'image/png'; - '.pnm' = 'image/x-portable-anymap'; - '.pnt' = 'image/x-macpaint'; - '.pntg' = 'image/x-macpaint'; - '.pnz' = 'image/png'; - '.pode' = 'application/PowerShell'; - '.pot' = 'application/vnd.ms-powerpoint'; - '.potm' = 'application/vnd.ms-powerpoint.template.macroEnabled.12'; - '.potx' = 'application/vnd.openxmlformats-officedocument.presentationml.template'; - '.ppa' = 'application/vnd.ms-powerpoint'; - '.ppam' = 'application/vnd.ms-powerpoint.addin.macroEnabled.12'; - '.ppm' = 'image/x-portable-pixmap'; - '.pps' = 'application/vnd.ms-powerpoint'; - '.ppsm' = 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12'; - '.ppsx' = 'application/vnd.openxmlformats-officedocument.presentationml.slideshow'; - '.ppt' = 'application/vnd.ms-powerpoint'; - '.pptm' = 'application/vnd.ms-powerpoint.presentation.macroEnabled.12'; - '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; - '.prf' = 'application/pics-rules'; - '.prm' = 'application/octet-stream'; - '.prx' = 'application/octet-stream'; - '.ps' = 'application/postscript'; - '.ps1' = 'application/PowerShell'; - '.psc1' = 'application/PowerShell'; - '.psd1' = 'application/PowerShell'; - '.psm1' = 'application/PowerShell'; - '.psd' = 'application/octet-stream'; - '.psess' = 'application/xml'; - '.psm' = 'application/octet-stream'; - '.psp' = 'application/octet-stream'; - '.pst' = 'application/vnd.ms-outlook'; - '.pub' = 'application/x-mspublisher'; - '.pwz' = 'application/vnd.ms-powerpoint'; - '.qht' = 'text/x-html-insertion'; - '.qhtm' = 'text/x-html-insertion'; - '.qt' = 'video/quicktime'; - '.qti' = 'image/x-quicktime'; - '.qtif' = 'image/x-quicktime'; - '.qtl' = 'application/x-quicktimeplayer'; - '.qxd' = 'application/octet-stream'; - '.ra' = 'audio/x-pn-realaudio'; - '.ram' = 'audio/x-pn-realaudio'; - '.rar' = 'application/x-rar-compressed'; - '.ras' = 'image/x-cmu-raster'; - '.rat' = 'application/rat-file'; - '.rc' = 'text/plain'; - '.rc2' = 'text/plain'; - '.rct' = 'text/plain'; - '.rdlc' = 'application/xml'; - '.reg' = 'text/plain'; - '.resx' = 'application/xml'; - '.rf' = 'image/vnd.rn-realflash'; - '.rgb' = 'image/x-rgb'; - '.rgs' = 'text/plain'; - '.rm' = 'application/vnd.rn-realmedia'; - '.rmi' = 'audio/mid'; - '.rmp' = 'application/vnd.rn-rn_music_package'; - '.roff' = 'application/x-troff'; - '.rpm' = 'audio/x-pn-realaudio-plugin'; - '.rqy' = 'text/x-ms-rqy'; - '.rtf' = 'application/rtf'; - '.rtx' = 'text/richtext'; - '.rvt' = 'application/octet-stream' ; - '.ruleset' = 'application/xml'; - '.s' = 'text/plain'; - '.safariextz' = 'application/x-safari-safariextz'; - '.scd' = 'application/x-msschedule'; - '.scr' = 'text/plain'; - '.sct' = 'text/scriptlet'; - '.sd2' = 'audio/x-sd2'; - '.sdp' = 'application/sdp'; - '.sea' = 'application/octet-stream'; - '.searchconnector-ms' = 'application/windows-search-connector+xml'; - '.setpay' = 'application/set-payment-initiation'; - '.setreg' = 'application/set-registration-initiation'; - '.settings' = 'application/xml'; - '.sgimb' = 'application/x-sgimb'; - '.sgml' = 'text/sgml'; - '.sh' = 'application/x-sh'; - '.shar' = 'application/x-shar'; - '.shtml' = 'text/html'; - '.sit' = 'application/x-stuffit'; - '.sitemap' = 'application/xml'; - '.skin' = 'application/xml'; - '.skp' = 'application/x-koan' ; - '.sldm' = 'application/vnd.ms-powerpoint.slide.macroEnabled.12'; - '.sldx' = 'application/vnd.openxmlformats-officedocument.presentationml.slide'; - '.slk' = 'application/vnd.ms-excel'; - '.sln' = 'text/plain'; - '.slupkg-ms' = 'application/x-ms-license'; - '.smd' = 'audio/x-smd'; - '.smi' = 'application/octet-stream'; - '.smx' = 'audio/x-smd'; - '.smz' = 'audio/x-smd'; - '.snd' = 'audio/basic'; - '.snippet' = 'application/xml'; - '.snp' = 'application/octet-stream'; - '.sol' = 'text/plain'; - '.sor' = 'text/plain'; - '.spc' = 'application/x-pkcs7-certificates'; - '.spl' = 'application/futuresplash'; - '.spx' = 'audio/ogg'; - '.src' = 'application/x-wais-source'; - '.srf' = 'text/plain'; - '.ssisdeploymentmanifest' = 'text/xml'; - '.ssm' = 'application/streamingmedia'; - '.sst' = 'application/vnd.ms-pki.certstore'; - '.stl' = 'application/vnd.ms-pki.stl'; - '.sv4cpio' = 'application/x-sv4cpio'; - '.sv4crc' = 'application/x-sv4crc'; - '.svc' = 'application/xml'; - '.svg' = 'image/svg+xml'; - '.swf' = 'application/x-shockwave-flash'; - '.step' = 'application/step'; - '.stp' = 'application/step'; - '.t' = 'application/x-troff'; - '.tar' = 'application/x-tar'; - '.tcl' = 'application/x-tcl'; - '.testrunconfig' = 'application/xml'; - '.testsettings' = 'application/xml'; - '.tex' = 'application/x-tex'; - '.texi' = 'application/x-texinfo'; - '.texinfo' = 'application/x-texinfo'; - '.tgz' = 'application/x-compressed'; - '.thmx' = 'application/vnd.ms-officetheme'; - '.thn' = 'application/octet-stream'; - '.tif' = 'image/tiff'; - '.tiff' = 'image/tiff'; - '.tlh' = 'text/plain'; - '.tli' = 'text/plain'; - '.toc' = 'application/octet-stream'; - '.tr' = 'application/x-troff'; - '.trm' = 'application/x-msterminal'; - '.trx' = 'application/xml'; - '.ts' = 'video/vnd.dlna.mpeg-tts'; - '.tsv' = 'text/tab-separated-values'; - '.ttf' = 'application/font-sfnt'; - '.tts' = 'video/vnd.dlna.mpeg-tts'; - '.txt' = 'text/plain'; - '.u32' = 'application/octet-stream'; - '.uls' = 'text/iuls'; - '.user' = 'text/plain'; - '.ustar' = 'application/x-ustar'; - '.vb' = 'text/plain'; - '.vbdproj' = 'text/plain'; - '.vbk' = 'video/mpeg'; - '.vbproj' = 'text/plain'; - '.vbs' = 'text/vbscript'; - '.vcf' = 'text/x-vcard'; - '.vcproj' = 'application/xml'; - '.vcs' = 'text/plain'; - '.vcxproj' = 'application/xml'; - '.vddproj' = 'text/plain'; - '.vdp' = 'text/plain'; - '.vdproj' = 'text/plain'; - '.vdx' = 'application/vnd.ms-visio.viewer'; - '.vml' = 'text/xml'; - '.vscontent' = 'application/xml'; - '.vsct' = 'text/xml'; - '.vsd' = 'application/vnd.visio'; - '.vsi' = 'application/ms-vsi'; - '.vsix' = 'application/vsix'; - '.vsixlangpack' = 'text/xml'; - '.vsixmanifest' = 'text/xml'; - '.vsmdi' = 'application/xml'; - '.vspscc' = 'text/plain'; - '.vss' = 'application/vnd.visio'; - '.vsscc' = 'text/plain'; - '.vssettings' = 'text/xml'; - '.vssscc' = 'text/plain'; - '.vst' = 'application/vnd.visio'; - '.vstemplate' = 'text/xml'; - '.vsto' = 'application/x-ms-vsto'; - '.vsw' = 'application/vnd.visio'; - '.vsx' = 'application/vnd.visio'; - '.vtx' = 'application/vnd.visio'; - '.wasm' = 'application/wasm'; - '.wav' = 'audio/wav'; - '.wave' = 'audio/wav'; - '.wax' = 'audio/x-ms-wax'; - '.wbk' = 'application/msword'; - '.wbmp' = 'image/vnd.wap.wbmp'; - '.wcm' = 'application/vnd.ms-works'; - '.wdb' = 'application/vnd.ms-works'; - '.wdp' = 'image/vnd.ms-photo'; - '.webarchive' = 'application/x-safari-webarchive'; - '.webm' = 'video/webm'; - '.webp' = 'image/webp'; - '.webtest' = 'application/xml'; - '.wiq' = 'application/xml'; - '.wiz' = 'application/msword'; - '.wks' = 'application/vnd.ms-works'; - '.wlmp' = 'application/wlmoviemaker'; - '.wlpginstall' = 'application/x-wlpg-detect'; - '.wlpginstall3' = 'application/x-wlpg3-detect'; - '.wm' = 'video/x-ms-wm'; - '.wma' = 'audio/x-ms-wma'; - '.wmd' = 'application/x-ms-wmd'; - '.wmf' = 'application/x-msmetafile'; - '.wml' = 'text/vnd.wap.wml'; - '.wmlc' = 'application/vnd.wap.wmlc'; - '.wmls' = 'text/vnd.wap.wmlscript'; - '.wmlsc' = 'application/vnd.wap.wmlscriptc'; - '.wmp' = 'video/x-ms-wmp'; - '.wmv' = 'video/x-ms-wmv'; - '.wmx' = 'video/x-ms-wmx'; - '.wmz' = 'application/x-ms-wmz'; - '.woff' = 'application/font-woff'; - '.woff2' = 'application/font-woff2'; - '.wpl' = 'application/vnd.ms-wpl'; - '.wps' = 'application/vnd.ms-works'; - '.wri' = 'application/x-mswrite'; - '.wrl' = 'x-world/x-vrml'; - '.wrz' = 'x-world/x-vrml'; - '.wsc' = 'text/scriptlet'; - '.wsdl' = 'text/xml'; - '.wvx' = 'video/x-ms-wvx'; - '.x' = 'application/directx'; - '.xaf' = 'x-world/x-vrml'; - '.xaml' = 'application/xaml+xml'; - '.xap' = 'application/x-silverlight-app'; - '.xbap' = 'application/x-ms-xbap'; - '.xbm' = 'image/x-xbitmap'; - '.xdr' = 'text/plain'; - '.xht' = 'application/xhtml+xml'; - '.xhtml' = 'application/xhtml+xml'; - '.xla' = 'application/vnd.ms-excel'; - '.xlam' = 'application/vnd.ms-excel.addin.macroEnabled.12'; - '.xlc' = 'application/vnd.ms-excel'; - '.xld' = 'application/vnd.ms-excel'; - '.xlk' = 'application/vnd.ms-excel'; - '.xll' = 'application/vnd.ms-excel'; - '.xlm' = 'application/vnd.ms-excel'; - '.xls' = 'application/vnd.ms-excel'; - '.xlsb' = 'application/vnd.ms-excel.sheet.binary.macroEnabled.12'; - '.xlsm' = 'application/vnd.ms-excel.sheet.macroEnabled.12'; - '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - '.xlt' = 'application/vnd.ms-excel'; - '.xltm' = 'application/vnd.ms-excel.template.macroEnabled.12'; - '.xltx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'; - '.xlw' = 'application/vnd.ms-excel'; - '.xml' = 'text/xml'; - '.xmp' = 'application/octet-stream' ; - '.xmta' = 'application/xml'; - '.xof' = 'x-world/x-vrml'; - '.xoml' = 'text/plain'; - '.xpm' = 'image/x-xpixmap'; - '.xps' = 'application/vnd.ms-xpsdocument'; - '.xrm-ms' = 'text/xml'; - '.xsc' = 'application/xml'; - '.xsd' = 'text/xml'; - '.xsf' = 'text/xml'; - '.xsl' = 'text/xml'; - '.xslt' = 'text/xml'; - '.xsn' = 'application/octet-stream'; - '.xss' = 'application/xml'; - '.xspf' = 'application/xspf+xml'; - '.xtp' = 'application/octet-stream'; - '.xwd' = 'image/x-xwindowdump'; - '.yaml' = 'application/x-yaml'; - '.yml' = 'application/x-yaml'; - '.z' = 'application/x-compress'; - '.zip' = 'application/zip'; - } + Describe 'All Extension Loop' { + BeforeAll { + $types = @{ + '.323' = 'text/h323' + '.3g2' = 'video/3gpp2' + '.3gp' = 'video/3gpp' + '.3gp2' = 'video/3gpp2' + '.3gpp' = 'video/3gpp' + '.7z' = 'application/x-7z-compressed' + '.aa' = 'audio/audible' + '.aac' = 'audio/aac' + '.aaf' = 'application/octet-stream' + '.aax' = 'audio/vnd.audible.aax' + '.ac3' = 'audio/ac3' + '.aca' = 'application/octet-stream' + '.accda' = 'application/msaccess.addin' + '.accdb' = 'application/msaccess' + '.accdc' = 'application/msaccess.cab' + '.accde' = 'application/msaccess' + '.accdr' = 'application/msaccess.runtime' + '.accdt' = 'application/msaccess' + '.accdw' = 'application/msaccess.webapplication' + '.accft' = 'application/msaccess.ftemplate' + '.acx' = 'application/internet-property-stream' + '.addin' = 'text/xml' + '.ade' = 'application/msaccess' + '.adobebridge' = 'application/x-bridge-url' + '.adp' = 'application/msaccess' + '.adt' = 'audio/vnd.dlna.adts' + '.adts' = 'audio/aac' + '.afm' = 'application/octet-stream' + '.ai' = 'application/postscript' + '.aif' = 'audio/aiff' + '.aifc' = 'audio/aiff' + '.aiff' = 'audio/aiff' + '.air' = 'application/vnd.adobe.air-application-installer-package+zip' + '.amc' = 'application/mpeg' + '.anx' = 'application/annodex' + '.apk' = 'application/vnd.android.package-archive' + '.application' = 'application/x-ms-application' + '.art' = 'image/x-jg' + '.asa' = 'application/xml' + '.asax' = 'application/xml' + '.ascx' = 'application/xml' + '.asd' = 'application/octet-stream' + '.asf' = 'video/x-ms-asf' + '.ashx' = 'application/xml' + '.asi' = 'application/octet-stream' + '.asm' = 'text/plain' + '.asmx' = 'application/xml' + '.aspx' = 'application/xml' + '.asr' = 'video/x-ms-asf' + '.asx' = 'video/x-ms-asf' + '.atom' = 'application/atom+xml' + '.au' = 'audio/basic' + '.avi' = 'video/x-msvideo' + '.axa' = 'audio/annodex' + '.axs' = 'application/olescript' + '.axv' = 'video/annodex' + '.bas' = 'text/plain' + '.bcpio' = 'application/x-bcpio' + '.bin' = 'application/octet-stream' + '.bmp' = 'image/bmp' + '.c' = 'text/plain' + '.cab' = 'application/octet-stream' + '.caf' = 'audio/x-caf' + '.calx' = 'application/vnd.ms-office.calx' + '.cat' = 'application/vnd.ms-pki.seccat' + '.cc' = 'text/plain' + '.cd' = 'text/plain' + '.cdda' = 'audio/aiff' + '.cdf' = 'application/x-cdf' + '.cer' = 'application/x-x509-ca-cert' + '.cfg' = 'text/plain' + '.chm' = 'application/octet-stream' + '.class' = 'application/x-java-applet' + '.clp' = 'application/x-msclip' + '.cmd' = 'text/plain' + '.cmx' = 'image/x-cmx' + '.cnf' = 'text/plain' + '.cod' = 'image/cis-cod' + '.config' = 'application/xml' + '.contact' = 'text/x-ms-contact' + '.coverage' = 'application/xml' + '.cpio' = 'application/x-cpio' + '.cpp' = 'text/plain' + '.crd' = 'application/x-mscardfile' + '.crl' = 'application/pkix-crl' + '.crt' = 'application/x-x509-ca-cert' + '.cs' = 'text/plain' + '.csdproj' = 'text/plain' + '.csh' = 'application/x-csh' + '.csproj' = 'text/plain' + '.css' = 'text/css' + '.csv' = 'text/csv' + '.cur' = 'application/octet-stream' + '.cxx' = 'text/plain' + '.dat' = 'application/octet-stream' + '.datasource' = 'application/xml' + '.dbproj' = 'text/plain' + '.dcr' = 'application/x-director' + '.def' = 'text/plain' + '.deploy' = 'application/octet-stream' + '.der' = 'application/x-x509-ca-cert' + '.dgml' = 'application/xml' + '.dib' = 'image/bmp' + '.dif' = 'video/x-dv' + '.dir' = 'application/x-director' + '.disco' = 'text/xml' + '.divx' = 'video/divx' + '.dll' = 'application/x-msdownload' + '.dll.config' = 'text/xml' + '.dlm' = 'text/dlm' + '.doc' = 'application/msword' + '.docm' = 'application/vnd.ms-word.document.macroEnabled.12' + '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + '.dot' = 'application/msword' + '.dotm' = 'application/vnd.ms-word.template.macroEnabled.12' + '.dotx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' + '.dsp' = 'application/octet-stream' + '.dsw' = 'text/plain' + '.dtd' = 'text/xml' + '.dtsconfig' = 'text/xml' + '.dv' = 'video/x-dv' + '.dvi' = 'application/x-dvi' + '.dwf' = 'drawing/x-dwf' + '.dwg' = 'application/acad' + '.dwp' = 'application/octet-stream' + '.dxf' = 'application/x-dxf' + '.dxr' = 'application/x-director' + '.eml' = 'message/rfc822' + '.emz' = 'application/octet-stream' + '.eot' = 'application/vnd.ms-fontobject' + '.eps' = 'application/postscript' + '.etl' = 'application/etl' + '.etx' = 'text/x-setext' + '.evy' = 'application/envoy' + '.exe' = 'application/octet-stream' + '.exe.config' = 'text/xml' + '.fdf' = 'application/vnd.fdf' + '.fif' = 'application/fractals' + '.filters' = 'application/xml' + '.fla' = 'application/octet-stream' + '.flac' = 'audio/flac' + '.flr' = 'x-world/x-vrml' + '.flv' = 'video/x-flv' + '.fsscript' = 'application/fsharp-script' + '.fsx' = 'application/fsharp-script' + '.generictest' = 'application/xml' + '.gif' = 'image/gif' + '.gpx' = 'application/gpx+xml' + '.group' = 'text/x-ms-group' + '.gsm' = 'audio/x-gsm' + '.gtar' = 'application/x-gtar' + '.gz' = 'application/x-gzip' + '.h' = 'text/plain' + '.hdf' = 'application/x-hdf' + '.hdml' = 'text/x-hdml' + '.hhc' = 'application/x-oleobject' + '.hhk' = 'application/octet-stream' + '.hhp' = 'application/octet-stream' + '.hlp' = 'application/winhlp' + '.hpp' = 'text/plain' + '.hqx' = 'application/mac-binhex40' + '.hta' = 'application/hta' + '.htc' = 'text/x-component' + '.htm' = 'text/html' + '.html' = 'text/html' + '.htt' = 'text/webviewhtml' + '.hxa' = 'application/xml' + '.hxc' = 'application/xml' + '.hxd' = 'application/octet-stream' + '.hxe' = 'application/xml' + '.hxf' = 'application/xml' + '.hxh' = 'application/octet-stream' + '.hxi' = 'application/octet-stream' + '.hxk' = 'application/xml' + '.hxq' = 'application/octet-stream' + '.hxr' = 'application/octet-stream' + '.hxs' = 'application/octet-stream' + '.hxt' = 'text/html' + '.hxv' = 'application/xml' + '.hxw' = 'application/octet-stream' + '.hxx' = 'text/plain' + '.i' = 'text/plain' + '.ico' = 'image/x-icon' + '.ics' = 'application/octet-stream' + '.idl' = 'text/plain' + '.ief' = 'image/ief' + '.iii' = 'application/x-iphone' + '.inc' = 'text/plain' + '.inf' = 'application/octet-stream' + '.ini' = 'text/plain' + '.inl' = 'text/plain' + '.ins' = 'application/x-internet-signup' + '.ipa' = 'application/x-itunes-ipa' + '.ipg' = 'application/x-itunes-ipg' + '.ipproj' = 'text/plain' + '.ipsw' = 'application/x-itunes-ipsw' + '.iqy' = 'text/x-ms-iqy' + '.isp' = 'application/x-internet-signup' + '.ite' = 'application/x-itunes-ite' + '.itlp' = 'application/x-itunes-itlp' + '.itms' = 'application/x-itunes-itms' + '.itpc' = 'application/x-itunes-itpc' + '.ivf' = 'video/x-ivf' + '.jar' = 'application/java-archive' + '.java' = 'application/octet-stream' + '.jck' = 'application/liquidmotion' + '.jcz' = 'application/liquidmotion' + '.jfif' = 'image/pjpeg' + '.jnlp' = 'application/x-java-jnlp-file' + '.jpb' = 'application/octet-stream' + '.jpe' = 'image/jpeg' + '.jpeg' = 'image/jpeg' + '.jpg' = 'image/jpeg' + '.js' = 'application/javascript' + '.json' = 'application/json' + '.jsx' = 'text/jscript' + '.jsxbin' = 'text/plain' + '.latex' = 'application/x-latex' + '.library-ms' = 'application/windows-library+xml' + '.lit' = 'application/x-ms-reader' + '.loadtest' = 'application/xml' + '.lpk' = 'application/octet-stream' + '.lsf' = 'video/x-la-asf' + '.lst' = 'text/plain' + '.lsx' = 'video/x-la-asf' + '.lzh' = 'application/octet-stream' + '.m13' = 'application/x-msmediaview' + '.m14' = 'application/x-msmediaview' + '.m1v' = 'video/mpeg' + '.m2t' = 'video/vnd.dlna.mpeg-tts' + '.m2ts' = 'video/vnd.dlna.mpeg-tts' + '.m2v' = 'video/mpeg' + '.m3u' = 'audio/x-mpegurl' + '.m3u8' = 'audio/x-mpegurl' + '.m4a' = 'audio/m4a' + '.m4b' = 'audio/m4b' + '.m4p' = 'audio/m4p' + '.m4r' = 'audio/x-m4r' + '.m4v' = 'video/x-m4v' + '.mac' = 'image/x-macpaint' + '.mak' = 'text/plain' + '.man' = 'application/x-troff-man' + '.manifest' = 'application/x-ms-manifest' + '.map' = 'text/plain' + '.markdown' = 'text/markdown' + '.master' = 'application/xml' + '.mbox' = 'application/mbox' + '.md' = 'text/markdown' + '.mda' = 'application/msaccess' + '.mdb' = 'application/x-msaccess' + '.mde' = 'application/msaccess' + '.mdp' = 'application/octet-stream' + '.me' = 'application/x-troff-me' + '.mfp' = 'application/x-shockwave-flash' + '.mht' = 'message/rfc822' + '.mhtml' = 'message/rfc822' + '.mid' = 'audio/mid' + '.midi' = 'audio/mid' + '.mix' = 'application/octet-stream' + '.mk' = 'text/plain' + '.mk3d' = 'video/x-matroska-3d' + '.mka' = 'audio/x-matroska' + '.mkv' = 'video/x-matroska' + '.mmf' = 'application/x-smaf' + '.mno' = 'text/xml' + '.mny' = 'application/x-msmoney' + '.mod' = 'video/mpeg' + '.mov' = 'video/quicktime' + '.movie' = 'video/x-sgi-movie' + '.mp2' = 'video/mpeg' + '.mp2v' = 'video/mpeg' + '.mp3' = 'audio/mpeg' + '.mp4' = 'video/mp4' + '.mp4v' = 'video/mp4' + '.mpa' = 'video/mpeg' + '.mpe' = 'video/mpeg' + '.mpeg' = 'video/mpeg' + '.mpf' = 'application/vnd.ms-mediapackage' + '.mpg' = 'video/mpeg' + '.mpp' = 'application/vnd.ms-project' + '.mpv2' = 'video/mpeg' + '.mqv' = 'video/quicktime' + '.ms' = 'application/x-troff-ms' + '.msg' = 'application/vnd.ms-outlook' + '.msi' = 'application/octet-stream' + '.mso' = 'application/octet-stream' + '.mts' = 'video/vnd.dlna.mpeg-tts' + '.mtx' = 'application/xml' + '.mvb' = 'application/x-msmediaview' + '.mvc' = 'application/x-miva-compiled' + '.mxp' = 'application/x-mmxp' + '.nc' = 'application/x-netcdf' + '.nsc' = 'video/x-ms-asf' + '.nws' = 'message/rfc822' + '.ocx' = 'application/octet-stream' + '.oda' = 'application/oda' + '.odb' = 'application/vnd.oasis.opendocument.database' + '.odc' = 'application/vnd.oasis.opendocument.chart' + '.odf' = 'application/vnd.oasis.opendocument.formula' + '.odg' = 'application/vnd.oasis.opendocument.graphics' + '.odh' = 'text/plain' + '.odi' = 'application/vnd.oasis.opendocument.image' + '.odl' = 'text/plain' + '.odm' = 'application/vnd.oasis.opendocument.text-master' + '.odp' = 'application/vnd.oasis.opendocument.presentation' + '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' + '.odt' = 'application/vnd.oasis.opendocument.text' + '.oga' = 'audio/ogg' + '.ogg' = 'audio/ogg' + '.ogv' = 'video/ogg' + '.ogx' = 'application/ogg' + '.one' = 'application/onenote' + '.onea' = 'application/onenote' + '.onepkg' = 'application/onenote' + '.onetmp' = 'application/onenote' + '.onetoc' = 'application/onenote' + '.onetoc2' = 'application/onenote' + '.opus' = 'audio/ogg' + '.orderedtest' = 'application/xml' + '.osdx' = 'application/opensearchdescription+xml' + '.otf' = 'application/font-sfnt' + '.otg' = 'application/vnd.oasis.opendocument.graphics-template' + '.oth' = 'application/vnd.oasis.opendocument.text-web' + '.otp' = 'application/vnd.oasis.opendocument.presentation-template' + '.ots' = 'application/vnd.oasis.opendocument.spreadsheet-template' + '.ott' = 'application/vnd.oasis.opendocument.text-template' + '.oxt' = 'application/vnd.openofficeorg.extension' + '.p10' = 'application/pkcs10' + '.p12' = 'application/x-pkcs12' + '.p7b' = 'application/x-pkcs7-certificates' + '.p7c' = 'application/pkcs7-mime' + '.p7m' = 'application/pkcs7-mime' + '.p7r' = 'application/x-pkcs7-certreqresp' + '.p7s' = 'application/pkcs7-signature' + '.pbm' = 'image/x-portable-bitmap' + '.pcast' = 'application/x-podcast' + '.pct' = 'image/pict' + '.pcx' = 'application/octet-stream' + '.pcz' = 'application/octet-stream' + '.pdf' = 'application/pdf' + '.pfb' = 'application/octet-stream' + '.pfm' = 'application/octet-stream' + '.pfx' = 'application/x-pkcs12' + '.pgm' = 'image/x-portable-graymap' + '.pic' = 'image/pict' + '.pict' = 'image/pict' + '.pkgdef' = 'text/plain' + '.pkgundef' = 'text/plain' + '.pko' = 'application/vnd.ms-pki.pko' + '.pls' = 'audio/scpls' + '.pma' = 'application/x-perfmon' + '.pmc' = 'application/x-perfmon' + '.pml' = 'application/x-perfmon' + '.pmr' = 'application/x-perfmon' + '.pmw' = 'application/x-perfmon' + '.png' = 'image/png' + '.pnm' = 'image/x-portable-anymap' + '.pnt' = 'image/x-macpaint' + '.pntg' = 'image/x-macpaint' + '.pnz' = 'image/png' + '.pode' = 'application/PowerShell' + '.pot' = 'application/vnd.ms-powerpoint' + '.potm' = 'application/vnd.ms-powerpoint.template.macroEnabled.12' + '.potx' = 'application/vnd.openxmlformats-officedocument.presentationml.template' + '.ppa' = 'application/vnd.ms-powerpoint' + '.ppam' = 'application/vnd.ms-powerpoint.addin.macroEnabled.12' + '.ppm' = 'image/x-portable-pixmap' + '.pps' = 'application/vnd.ms-powerpoint' + '.ppsm' = 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' + '.ppsx' = 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' + '.ppt' = 'application/vnd.ms-powerpoint' + '.pptm' = 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' + '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + '.prf' = 'application/pics-rules' + '.prm' = 'application/octet-stream' + '.prx' = 'application/octet-stream' + '.ps' = 'application/postscript' + '.ps1' = 'application/PowerShell' + '.psc1' = 'application/PowerShell' + '.psd1' = 'application/PowerShell' + '.psm1' = 'application/PowerShell' + '.psd' = 'application/octet-stream' + '.psess' = 'application/xml' + '.psm' = 'application/octet-stream' + '.psp' = 'application/octet-stream' + '.pst' = 'application/vnd.ms-outlook' + '.pub' = 'application/x-mspublisher' + '.pwz' = 'application/vnd.ms-powerpoint' + '.qht' = 'text/x-html-insertion' + '.qhtm' = 'text/x-html-insertion' + '.qt' = 'video/quicktime' + '.qti' = 'image/x-quicktime' + '.qtif' = 'image/x-quicktime' + '.qtl' = 'application/x-quicktimeplayer' + '.qxd' = 'application/octet-stream' + '.ra' = 'audio/x-pn-realaudio' + '.ram' = 'audio/x-pn-realaudio' + '.rar' = 'application/x-rar-compressed' + '.ras' = 'image/x-cmu-raster' + '.rat' = 'application/rat-file' + '.rc' = 'text/plain' + '.rc2' = 'text/plain' + '.rct' = 'text/plain' + '.rdlc' = 'application/xml' + '.reg' = 'text/plain' + '.resx' = 'application/xml' + '.rf' = 'image/vnd.rn-realflash' + '.rgb' = 'image/x-rgb' + '.rgs' = 'text/plain' + '.rm' = 'application/vnd.rn-realmedia' + '.rmi' = 'audio/mid' + '.rmp' = 'application/vnd.rn-rn_music_package' + '.roff' = 'application/x-troff' + '.rpm' = 'audio/x-pn-realaudio-plugin' + '.rqy' = 'text/x-ms-rqy' + '.rtf' = 'application/rtf' + '.rtx' = 'text/richtext' + '.rvt' = 'application/octet-stream' + '.ruleset' = 'application/xml' + '.s' = 'text/plain' + '.safariextz' = 'application/x-safari-safariextz' + '.scd' = 'application/x-msschedule' + '.scr' = 'text/plain' + '.sct' = 'text/scriptlet' + '.sd2' = 'audio/x-sd2' + '.sdp' = 'application/sdp' + '.sea' = 'application/octet-stream' + '.searchconnector-ms' = 'application/windows-search-connector+xml' + '.setpay' = 'application/set-payment-initiation' + '.setreg' = 'application/set-registration-initiation' + '.settings' = 'application/xml' + '.sgimb' = 'application/x-sgimb' + '.sgml' = 'text/sgml' + '.sh' = 'application/x-sh' + '.shar' = 'application/x-shar' + '.shtml' = 'text/html' + '.sit' = 'application/x-stuffit' + '.sitemap' = 'application/xml' + '.skin' = 'application/xml' + '.skp' = 'application/x-koan' + '.sldm' = 'application/vnd.ms-powerpoint.slide.macroEnabled.12' + '.sldx' = 'application/vnd.openxmlformats-officedocument.presentationml.slide' + '.slk' = 'application/vnd.ms-excel' + '.sln' = 'text/plain' + '.slupkg-ms' = 'application/x-ms-license' + '.smd' = 'audio/x-smd' + '.smi' = 'application/octet-stream' + '.smx' = 'audio/x-smd' + '.smz' = 'audio/x-smd' + '.snd' = 'audio/basic' + '.snippet' = 'application/xml' + '.snp' = 'application/octet-stream' + '.sol' = 'text/plain' + '.sor' = 'text/plain' + '.spc' = 'application/x-pkcs7-certificates' + '.spl' = 'application/futuresplash' + '.spx' = 'audio/ogg' + '.src' = 'application/x-wais-source' + '.srf' = 'text/plain' + '.ssisdeploymentmanifest' = 'text/xml' + '.ssm' = 'application/streamingmedia' + '.sst' = 'application/vnd.ms-pki.certstore' + '.stl' = 'application/vnd.ms-pki.stl' + '.sv4cpio' = 'application/x-sv4cpio' + '.sv4crc' = 'application/x-sv4crc' + '.svc' = 'application/xml' + '.svg' = 'image/svg+xml' + '.swf' = 'application/x-shockwave-flash' + '.step' = 'application/step' + '.stp' = 'application/step' + '.t' = 'application/x-troff' + '.tar' = 'application/x-tar' + '.tcl' = 'application/x-tcl' + '.testrunconfig' = 'application/xml' + '.testsettings' = 'application/xml' + '.tex' = 'application/x-tex' + '.texi' = 'application/x-texinfo' + '.texinfo' = 'application/x-texinfo' + '.tgz' = 'application/x-compressed' + '.thmx' = 'application/vnd.ms-officetheme' + '.thn' = 'application/octet-stream' + '.tif' = 'image/tiff' + '.tiff' = 'image/tiff' + '.tlh' = 'text/plain' + '.tli' = 'text/plain' + '.toc' = 'application/octet-stream' + '.tr' = 'application/x-troff' + '.trm' = 'application/x-msterminal' + '.trx' = 'application/xml' + '.ts' = 'video/vnd.dlna.mpeg-tts' + '.tsv' = 'text/tab-separated-values' + '.ttf' = 'application/font-sfnt' + '.tts' = 'video/vnd.dlna.mpeg-tts' + '.txt' = 'text/plain' + '.u32' = 'application/octet-stream' + '.uls' = 'text/iuls' + '.user' = 'text/plain' + '.ustar' = 'application/x-ustar' + '.vb' = 'text/plain' + '.vbdproj' = 'text/plain' + '.vbk' = 'video/mpeg' + '.vbproj' = 'text/plain' + '.vbs' = 'text/vbscript' + '.vcf' = 'text/x-vcard' + '.vcproj' = 'application/xml' + '.vcs' = 'text/plain' + '.vcxproj' = 'application/xml' + '.vddproj' = 'text/plain' + '.vdp' = 'text/plain' + '.vdproj' = 'text/plain' + '.vdx' = 'application/vnd.ms-visio.viewer' + '.vml' = 'text/xml' + '.vscontent' = 'application/xml' + '.vsct' = 'text/xml' + '.vsd' = 'application/vnd.visio' + '.vsi' = 'application/ms-vsi' + '.vsix' = 'application/vsix' + '.vsixlangpack' = 'text/xml' + '.vsixmanifest' = 'text/xml' + '.vsmdi' = 'application/xml' + '.vspscc' = 'text/plain' + '.vss' = 'application/vnd.visio' + '.vsscc' = 'text/plain' + '.vssettings' = 'text/xml' + '.vssscc' = 'text/plain' + '.vst' = 'application/vnd.visio' + '.vstemplate' = 'text/xml' + '.vsto' = 'application/x-ms-vsto' + '.vsw' = 'application/vnd.visio' + '.vsx' = 'application/vnd.visio' + '.vtx' = 'application/vnd.visio' + '.wasm' = 'application/wasm' + '.wav' = 'audio/wav' + '.wave' = 'audio/wav' + '.wax' = 'audio/x-ms-wax' + '.wbk' = 'application/msword' + '.wbmp' = 'image/vnd.wap.wbmp' + '.wcm' = 'application/vnd.ms-works' + '.wdb' = 'application/vnd.ms-works' + '.wdp' = 'image/vnd.ms-photo' + '.webarchive' = 'application/x-safari-webarchive' + '.webm' = 'video/webm' + '.webp' = 'image/webp' + '.webtest' = 'application/xml' + '.wiq' = 'application/xml' + '.wiz' = 'application/msword' + '.wks' = 'application/vnd.ms-works' + '.wlmp' = 'application/wlmoviemaker' + '.wlpginstall' = 'application/x-wlpg-detect' + '.wlpginstall3' = 'application/x-wlpg3-detect' + '.wm' = 'video/x-ms-wm' + '.wma' = 'audio/x-ms-wma' + '.wmd' = 'application/x-ms-wmd' + '.wmf' = 'application/x-msmetafile' + '.wml' = 'text/vnd.wap.wml' + '.wmlc' = 'application/vnd.wap.wmlc' + '.wmls' = 'text/vnd.wap.wmlscript' + '.wmlsc' = 'application/vnd.wap.wmlscriptc' + '.wmp' = 'video/x-ms-wmp' + '.wmv' = 'video/x-ms-wmv' + '.wmx' = 'video/x-ms-wmx' + '.wmz' = 'application/x-ms-wmz' + '.woff' = 'application/font-woff' + '.woff2' = 'application/font-woff2' + '.wpl' = 'application/vnd.ms-wpl' + '.wps' = 'application/vnd.ms-works' + '.wri' = 'application/x-mswrite' + '.wrl' = 'x-world/x-vrml' + '.wrz' = 'x-world/x-vrml' + '.wsc' = 'text/scriptlet' + '.wsdl' = 'text/xml' + '.wvx' = 'video/x-ms-wvx' + '.x' = 'application/directx' + '.xaf' = 'x-world/x-vrml' + '.xaml' = 'application/xaml+xml' + '.xap' = 'application/x-silverlight-app' + '.xbap' = 'application/x-ms-xbap' + '.xbm' = 'image/x-xbitmap' + '.xdr' = 'text/plain' + '.xht' = 'application/xhtml+xml' + '.xhtml' = 'application/xhtml+xml' + '.xla' = 'application/vnd.ms-excel' + '.xlam' = 'application/vnd.ms-excel.addin.macroEnabled.12' + '.xlc' = 'application/vnd.ms-excel' + '.xld' = 'application/vnd.ms-excel' + '.xlk' = 'application/vnd.ms-excel' + '.xll' = 'application/vnd.ms-excel' + '.xlm' = 'application/vnd.ms-excel' + '.xls' = 'application/vnd.ms-excel' + '.xlsb' = 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' + '.xlsm' = 'application/vnd.ms-excel.sheet.macroEnabled.12' + '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + '.xlt' = 'application/vnd.ms-excel' + '.xltm' = 'application/vnd.ms-excel.template.macroEnabled.12' + '.xltx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' + '.xlw' = 'application/vnd.ms-excel' + '.xml' = 'text/xml' + '.xmp' = 'application/octet-stream' + '.xmta' = 'application/xml' + '.xof' = 'x-world/x-vrml' + '.xoml' = 'text/plain' + '.xpm' = 'image/x-xpixmap' + '.xps' = 'application/vnd.ms-xpsdocument' + '.xrm-ms' = 'text/xml' + '.xsc' = 'application/xml' + '.xsd' = 'text/xml' + '.xsf' = 'text/xml' + '.xsl' = 'text/xml' + '.xslt' = 'text/xml' + '.xsn' = 'application/octet-stream' + '.xss' = 'application/xml' + '.xspf' = 'application/xspf+xml' + '.xtp' = 'application/octet-stream' + '.xwd' = 'image/x-xwindowdump' + '.yaml' = 'application/x-yaml' + '.yml' = 'application/x-yaml' + '.z' = 'application/x-compress' + '.zip' = 'application/zip' + } } + BeforeDiscovery { + $types = @{ + '.323' = 'text/h323' + '.3g2' = 'video/3gpp2' + '.3gp' = 'video/3gpp' + '.3gp2' = 'video/3gpp2' + '.3gpp' = 'video/3gpp' + '.7z' = 'application/x-7z-compressed' + '.aa' = 'audio/audible' + '.aac' = 'audio/aac' + '.aaf' = 'application/octet-stream' + '.aax' = 'audio/vnd.audible.aax' + '.ac3' = 'audio/ac3' + '.aca' = 'application/octet-stream' + '.accda' = 'application/msaccess.addin' + '.accdb' = 'application/msaccess' + '.accdc' = 'application/msaccess.cab' + '.accde' = 'application/msaccess' + '.accdr' = 'application/msaccess.runtime' + '.accdt' = 'application/msaccess' + '.accdw' = 'application/msaccess.webapplication' + '.accft' = 'application/msaccess.ftemplate' + '.acx' = 'application/internet-property-stream' + '.addin' = 'text/xml' + '.ade' = 'application/msaccess' + '.adobebridge' = 'application/x-bridge-url' + '.adp' = 'application/msaccess' + '.adt' = 'audio/vnd.dlna.adts' + '.adts' = 'audio/aac' + '.afm' = 'application/octet-stream' + '.ai' = 'application/postscript' + '.aif' = 'audio/aiff' + '.aifc' = 'audio/aiff' + '.aiff' = 'audio/aiff' + '.air' = 'application/vnd.adobe.air-application-installer-package+zip' + '.amc' = 'application/mpeg' + '.anx' = 'application/annodex' + '.apk' = 'application/vnd.android.package-archive' + '.application' = 'application/x-ms-application' + '.art' = 'image/x-jg' + '.asa' = 'application/xml' + '.asax' = 'application/xml' + '.ascx' = 'application/xml' + '.asd' = 'application/octet-stream' + '.asf' = 'video/x-ms-asf' + '.ashx' = 'application/xml' + '.asi' = 'application/octet-stream' + '.asm' = 'text/plain' + '.asmx' = 'application/xml' + '.aspx' = 'application/xml' + '.asr' = 'video/x-ms-asf' + '.asx' = 'video/x-ms-asf' + '.atom' = 'application/atom+xml' + '.au' = 'audio/basic' + '.avi' = 'video/x-msvideo' + '.axa' = 'audio/annodex' + '.axs' = 'application/olescript' + '.axv' = 'video/annodex' + '.bas' = 'text/plain' + '.bcpio' = 'application/x-bcpio' + '.bin' = 'application/octet-stream' + '.bmp' = 'image/bmp' + '.c' = 'text/plain' + '.cab' = 'application/octet-stream' + '.caf' = 'audio/x-caf' + '.calx' = 'application/vnd.ms-office.calx' + '.cat' = 'application/vnd.ms-pki.seccat' + '.cc' = 'text/plain' + '.cd' = 'text/plain' + '.cdda' = 'audio/aiff' + '.cdf' = 'application/x-cdf' + '.cer' = 'application/x-x509-ca-cert' + '.cfg' = 'text/plain' + '.chm' = 'application/octet-stream' + '.class' = 'application/x-java-applet' + '.clp' = 'application/x-msclip' + '.cmd' = 'text/plain' + '.cmx' = 'image/x-cmx' + '.cnf' = 'text/plain' + '.cod' = 'image/cis-cod' + '.config' = 'application/xml' + '.contact' = 'text/x-ms-contact' + '.coverage' = 'application/xml' + '.cpio' = 'application/x-cpio' + '.cpp' = 'text/plain' + '.crd' = 'application/x-mscardfile' + '.crl' = 'application/pkix-crl' + '.crt' = 'application/x-x509-ca-cert' + '.cs' = 'text/plain' + '.csdproj' = 'text/plain' + '.csh' = 'application/x-csh' + '.csproj' = 'text/plain' + '.css' = 'text/css' + '.csv' = 'text/csv' + '.cur' = 'application/octet-stream' + '.cxx' = 'text/plain' + '.dat' = 'application/octet-stream' + '.datasource' = 'application/xml' + '.dbproj' = 'text/plain' + '.dcr' = 'application/x-director' + '.def' = 'text/plain' + '.deploy' = 'application/octet-stream' + '.der' = 'application/x-x509-ca-cert' + '.dgml' = 'application/xml' + '.dib' = 'image/bmp' + '.dif' = 'video/x-dv' + '.dir' = 'application/x-director' + '.disco' = 'text/xml' + '.divx' = 'video/divx' + '.dll' = 'application/x-msdownload' + '.dll.config' = 'text/xml' + '.dlm' = 'text/dlm' + '.doc' = 'application/msword' + '.docm' = 'application/vnd.ms-word.document.macroEnabled.12' + '.docx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + '.dot' = 'application/msword' + '.dotm' = 'application/vnd.ms-word.template.macroEnabled.12' + '.dotx' = 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' + '.dsp' = 'application/octet-stream' + '.dsw' = 'text/plain' + '.dtd' = 'text/xml' + '.dtsconfig' = 'text/xml' + '.dv' = 'video/x-dv' + '.dvi' = 'application/x-dvi' + '.dwf' = 'drawing/x-dwf' + '.dwg' = 'application/acad' + '.dwp' = 'application/octet-stream' + '.dxf' = 'application/x-dxf' + '.dxr' = 'application/x-director' + '.eml' = 'message/rfc822' + '.emz' = 'application/octet-stream' + '.eot' = 'application/vnd.ms-fontobject' + '.eps' = 'application/postscript' + '.etl' = 'application/etl' + '.etx' = 'text/x-setext' + '.evy' = 'application/envoy' + '.exe' = 'application/octet-stream' + '.exe.config' = 'text/xml' + '.fdf' = 'application/vnd.fdf' + '.fif' = 'application/fractals' + '.filters' = 'application/xml' + '.fla' = 'application/octet-stream' + '.flac' = 'audio/flac' + '.flr' = 'x-world/x-vrml' + '.flv' = 'video/x-flv' + '.fsscript' = 'application/fsharp-script' + '.fsx' = 'application/fsharp-script' + '.generictest' = 'application/xml' + '.gif' = 'image/gif' + '.gpx' = 'application/gpx+xml' + '.group' = 'text/x-ms-group' + '.gsm' = 'audio/x-gsm' + '.gtar' = 'application/x-gtar' + '.gz' = 'application/x-gzip' + '.h' = 'text/plain' + '.hdf' = 'application/x-hdf' + '.hdml' = 'text/x-hdml' + '.hhc' = 'application/x-oleobject' + '.hhk' = 'application/octet-stream' + '.hhp' = 'application/octet-stream' + '.hlp' = 'application/winhlp' + '.hpp' = 'text/plain' + '.hqx' = 'application/mac-binhex40' + '.hta' = 'application/hta' + '.htc' = 'text/x-component' + '.htm' = 'text/html' + '.html' = 'text/html' + '.htt' = 'text/webviewhtml' + '.hxa' = 'application/xml' + '.hxc' = 'application/xml' + '.hxd' = 'application/octet-stream' + '.hxe' = 'application/xml' + '.hxf' = 'application/xml' + '.hxh' = 'application/octet-stream' + '.hxi' = 'application/octet-stream' + '.hxk' = 'application/xml' + '.hxq' = 'application/octet-stream' + '.hxr' = 'application/octet-stream' + '.hxs' = 'application/octet-stream' + '.hxt' = 'text/html' + '.hxv' = 'application/xml' + '.hxw' = 'application/octet-stream' + '.hxx' = 'text/plain' + '.i' = 'text/plain' + '.ico' = 'image/x-icon' + '.ics' = 'application/octet-stream' + '.idl' = 'text/plain' + '.ief' = 'image/ief' + '.iii' = 'application/x-iphone' + '.inc' = 'text/plain' + '.inf' = 'application/octet-stream' + '.ini' = 'text/plain' + '.inl' = 'text/plain' + '.ins' = 'application/x-internet-signup' + '.ipa' = 'application/x-itunes-ipa' + '.ipg' = 'application/x-itunes-ipg' + '.ipproj' = 'text/plain' + '.ipsw' = 'application/x-itunes-ipsw' + '.iqy' = 'text/x-ms-iqy' + '.isp' = 'application/x-internet-signup' + '.ite' = 'application/x-itunes-ite' + '.itlp' = 'application/x-itunes-itlp' + '.itms' = 'application/x-itunes-itms' + '.itpc' = 'application/x-itunes-itpc' + '.ivf' = 'video/x-ivf' + '.jar' = 'application/java-archive' + '.java' = 'application/octet-stream' + '.jck' = 'application/liquidmotion' + '.jcz' = 'application/liquidmotion' + '.jfif' = 'image/pjpeg' + '.jnlp' = 'application/x-java-jnlp-file' + '.jpb' = 'application/octet-stream' + '.jpe' = 'image/jpeg' + '.jpeg' = 'image/jpeg' + '.jpg' = 'image/jpeg' + '.js' = 'application/javascript' + '.json' = 'application/json' + '.jsx' = 'text/jscript' + '.jsxbin' = 'text/plain' + '.latex' = 'application/x-latex' + '.library-ms' = 'application/windows-library+xml' + '.lit' = 'application/x-ms-reader' + '.loadtest' = 'application/xml' + '.lpk' = 'application/octet-stream' + '.lsf' = 'video/x-la-asf' + '.lst' = 'text/plain' + '.lsx' = 'video/x-la-asf' + '.lzh' = 'application/octet-stream' + '.m13' = 'application/x-msmediaview' + '.m14' = 'application/x-msmediaview' + '.m1v' = 'video/mpeg' + '.m2t' = 'video/vnd.dlna.mpeg-tts' + '.m2ts' = 'video/vnd.dlna.mpeg-tts' + '.m2v' = 'video/mpeg' + '.m3u' = 'audio/x-mpegurl' + '.m3u8' = 'audio/x-mpegurl' + '.m4a' = 'audio/m4a' + '.m4b' = 'audio/m4b' + '.m4p' = 'audio/m4p' + '.m4r' = 'audio/x-m4r' + '.m4v' = 'video/x-m4v' + '.mac' = 'image/x-macpaint' + '.mak' = 'text/plain' + '.man' = 'application/x-troff-man' + '.manifest' = 'application/x-ms-manifest' + '.map' = 'text/plain' + '.markdown' = 'text/markdown' + '.master' = 'application/xml' + '.mbox' = 'application/mbox' + '.md' = 'text/markdown' + '.mda' = 'application/msaccess' + '.mdb' = 'application/x-msaccess' + '.mde' = 'application/msaccess' + '.mdp' = 'application/octet-stream' + '.me' = 'application/x-troff-me' + '.mfp' = 'application/x-shockwave-flash' + '.mht' = 'message/rfc822' + '.mhtml' = 'message/rfc822' + '.mid' = 'audio/mid' + '.midi' = 'audio/mid' + '.mix' = 'application/octet-stream' + '.mk' = 'text/plain' + '.mk3d' = 'video/x-matroska-3d' + '.mka' = 'audio/x-matroska' + '.mkv' = 'video/x-matroska' + '.mmf' = 'application/x-smaf' + '.mno' = 'text/xml' + '.mny' = 'application/x-msmoney' + '.mod' = 'video/mpeg' + '.mov' = 'video/quicktime' + '.movie' = 'video/x-sgi-movie' + '.mp2' = 'video/mpeg' + '.mp2v' = 'video/mpeg' + '.mp3' = 'audio/mpeg' + '.mp4' = 'video/mp4' + '.mp4v' = 'video/mp4' + '.mpa' = 'video/mpeg' + '.mpe' = 'video/mpeg' + '.mpeg' = 'video/mpeg' + '.mpf' = 'application/vnd.ms-mediapackage' + '.mpg' = 'video/mpeg' + '.mpp' = 'application/vnd.ms-project' + '.mpv2' = 'video/mpeg' + '.mqv' = 'video/quicktime' + '.ms' = 'application/x-troff-ms' + '.msg' = 'application/vnd.ms-outlook' + '.msi' = 'application/octet-stream' + '.mso' = 'application/octet-stream' + '.mts' = 'video/vnd.dlna.mpeg-tts' + '.mtx' = 'application/xml' + '.mvb' = 'application/x-msmediaview' + '.mvc' = 'application/x-miva-compiled' + '.mxp' = 'application/x-mmxp' + '.nc' = 'application/x-netcdf' + '.nsc' = 'video/x-ms-asf' + '.nws' = 'message/rfc822' + '.ocx' = 'application/octet-stream' + '.oda' = 'application/oda' + '.odb' = 'application/vnd.oasis.opendocument.database' + '.odc' = 'application/vnd.oasis.opendocument.chart' + '.odf' = 'application/vnd.oasis.opendocument.formula' + '.odg' = 'application/vnd.oasis.opendocument.graphics' + '.odh' = 'text/plain' + '.odi' = 'application/vnd.oasis.opendocument.image' + '.odl' = 'text/plain' + '.odm' = 'application/vnd.oasis.opendocument.text-master' + '.odp' = 'application/vnd.oasis.opendocument.presentation' + '.ods' = 'application/vnd.oasis.opendocument.spreadsheet' + '.odt' = 'application/vnd.oasis.opendocument.text' + '.oga' = 'audio/ogg' + '.ogg' = 'audio/ogg' + '.ogv' = 'video/ogg' + '.ogx' = 'application/ogg' + '.one' = 'application/onenote' + '.onea' = 'application/onenote' + '.onepkg' = 'application/onenote' + '.onetmp' = 'application/onenote' + '.onetoc' = 'application/onenote' + '.onetoc2' = 'application/onenote' + '.opus' = 'audio/ogg' + '.orderedtest' = 'application/xml' + '.osdx' = 'application/opensearchdescription+xml' + '.otf' = 'application/font-sfnt' + '.otg' = 'application/vnd.oasis.opendocument.graphics-template' + '.oth' = 'application/vnd.oasis.opendocument.text-web' + '.otp' = 'application/vnd.oasis.opendocument.presentation-template' + '.ots' = 'application/vnd.oasis.opendocument.spreadsheet-template' + '.ott' = 'application/vnd.oasis.opendocument.text-template' + '.oxt' = 'application/vnd.openofficeorg.extension' + '.p10' = 'application/pkcs10' + '.p12' = 'application/x-pkcs12' + '.p7b' = 'application/x-pkcs7-certificates' + '.p7c' = 'application/pkcs7-mime' + '.p7m' = 'application/pkcs7-mime' + '.p7r' = 'application/x-pkcs7-certreqresp' + '.p7s' = 'application/pkcs7-signature' + '.pbm' = 'image/x-portable-bitmap' + '.pcast' = 'application/x-podcast' + '.pct' = 'image/pict' + '.pcx' = 'application/octet-stream' + '.pcz' = 'application/octet-stream' + '.pdf' = 'application/pdf' + '.pfb' = 'application/octet-stream' + '.pfm' = 'application/octet-stream' + '.pfx' = 'application/x-pkcs12' + '.pgm' = 'image/x-portable-graymap' + '.pic' = 'image/pict' + '.pict' = 'image/pict' + '.pkgdef' = 'text/plain' + '.pkgundef' = 'text/plain' + '.pko' = 'application/vnd.ms-pki.pko' + '.pls' = 'audio/scpls' + '.pma' = 'application/x-perfmon' + '.pmc' = 'application/x-perfmon' + '.pml' = 'application/x-perfmon' + '.pmr' = 'application/x-perfmon' + '.pmw' = 'application/x-perfmon' + '.png' = 'image/png' + '.pnm' = 'image/x-portable-anymap' + '.pnt' = 'image/x-macpaint' + '.pntg' = 'image/x-macpaint' + '.pnz' = 'image/png' + '.pode' = 'application/PowerShell' + '.pot' = 'application/vnd.ms-powerpoint' + '.potm' = 'application/vnd.ms-powerpoint.template.macroEnabled.12' + '.potx' = 'application/vnd.openxmlformats-officedocument.presentationml.template' + '.ppa' = 'application/vnd.ms-powerpoint' + '.ppam' = 'application/vnd.ms-powerpoint.addin.macroEnabled.12' + '.ppm' = 'image/x-portable-pixmap' + '.pps' = 'application/vnd.ms-powerpoint' + '.ppsm' = 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12' + '.ppsx' = 'application/vnd.openxmlformats-officedocument.presentationml.slideshow' + '.ppt' = 'application/vnd.ms-powerpoint' + '.pptm' = 'application/vnd.ms-powerpoint.presentation.macroEnabled.12' + '.pptx' = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + '.prf' = 'application/pics-rules' + '.prm' = 'application/octet-stream' + '.prx' = 'application/octet-stream' + '.ps' = 'application/postscript' + '.ps1' = 'application/PowerShell' + '.psc1' = 'application/PowerShell' + '.psd1' = 'application/PowerShell' + '.psm1' = 'application/PowerShell' + '.psd' = 'application/octet-stream' + '.psess' = 'application/xml' + '.psm' = 'application/octet-stream' + '.psp' = 'application/octet-stream' + '.pst' = 'application/vnd.ms-outlook' + '.pub' = 'application/x-mspublisher' + '.pwz' = 'application/vnd.ms-powerpoint' + '.qht' = 'text/x-html-insertion' + '.qhtm' = 'text/x-html-insertion' + '.qt' = 'video/quicktime' + '.qti' = 'image/x-quicktime' + '.qtif' = 'image/x-quicktime' + '.qtl' = 'application/x-quicktimeplayer' + '.qxd' = 'application/octet-stream' + '.ra' = 'audio/x-pn-realaudio' + '.ram' = 'audio/x-pn-realaudio' + '.rar' = 'application/x-rar-compressed' + '.ras' = 'image/x-cmu-raster' + '.rat' = 'application/rat-file' + '.rc' = 'text/plain' + '.rc2' = 'text/plain' + '.rct' = 'text/plain' + '.rdlc' = 'application/xml' + '.reg' = 'text/plain' + '.resx' = 'application/xml' + '.rf' = 'image/vnd.rn-realflash' + '.rgb' = 'image/x-rgb' + '.rgs' = 'text/plain' + '.rm' = 'application/vnd.rn-realmedia' + '.rmi' = 'audio/mid' + '.rmp' = 'application/vnd.rn-rn_music_package' + '.roff' = 'application/x-troff' + '.rpm' = 'audio/x-pn-realaudio-plugin' + '.rqy' = 'text/x-ms-rqy' + '.rtf' = 'application/rtf' + '.rtx' = 'text/richtext' + '.rvt' = 'application/octet-stream' + '.ruleset' = 'application/xml' + '.s' = 'text/plain' + '.safariextz' = 'application/x-safari-safariextz' + '.scd' = 'application/x-msschedule' + '.scr' = 'text/plain' + '.sct' = 'text/scriptlet' + '.sd2' = 'audio/x-sd2' + '.sdp' = 'application/sdp' + '.sea' = 'application/octet-stream' + '.searchconnector-ms' = 'application/windows-search-connector+xml' + '.setpay' = 'application/set-payment-initiation' + '.setreg' = 'application/set-registration-initiation' + '.settings' = 'application/xml' + '.sgimb' = 'application/x-sgimb' + '.sgml' = 'text/sgml' + '.sh' = 'application/x-sh' + '.shar' = 'application/x-shar' + '.shtml' = 'text/html' + '.sit' = 'application/x-stuffit' + '.sitemap' = 'application/xml' + '.skin' = 'application/xml' + '.skp' = 'application/x-koan' + '.sldm' = 'application/vnd.ms-powerpoint.slide.macroEnabled.12' + '.sldx' = 'application/vnd.openxmlformats-officedocument.presentationml.slide' + '.slk' = 'application/vnd.ms-excel' + '.sln' = 'text/plain' + '.slupkg-ms' = 'application/x-ms-license' + '.smd' = 'audio/x-smd' + '.smi' = 'application/octet-stream' + '.smx' = 'audio/x-smd' + '.smz' = 'audio/x-smd' + '.snd' = 'audio/basic' + '.snippet' = 'application/xml' + '.snp' = 'application/octet-stream' + '.sol' = 'text/plain' + '.sor' = 'text/plain' + '.spc' = 'application/x-pkcs7-certificates' + '.spl' = 'application/futuresplash' + '.spx' = 'audio/ogg' + '.src' = 'application/x-wais-source' + '.srf' = 'text/plain' + '.ssisdeploymentmanifest' = 'text/xml' + '.ssm' = 'application/streamingmedia' + '.sst' = 'application/vnd.ms-pki.certstore' + '.stl' = 'application/vnd.ms-pki.stl' + '.sv4cpio' = 'application/x-sv4cpio' + '.sv4crc' = 'application/x-sv4crc' + '.svc' = 'application/xml' + '.svg' = 'image/svg+xml' + '.swf' = 'application/x-shockwave-flash' + '.step' = 'application/step' + '.stp' = 'application/step' + '.t' = 'application/x-troff' + '.tar' = 'application/x-tar' + '.tcl' = 'application/x-tcl' + '.testrunconfig' = 'application/xml' + '.testsettings' = 'application/xml' + '.tex' = 'application/x-tex' + '.texi' = 'application/x-texinfo' + '.texinfo' = 'application/x-texinfo' + '.tgz' = 'application/x-compressed' + '.thmx' = 'application/vnd.ms-officetheme' + '.thn' = 'application/octet-stream' + '.tif' = 'image/tiff' + '.tiff' = 'image/tiff' + '.tlh' = 'text/plain' + '.tli' = 'text/plain' + '.toc' = 'application/octet-stream' + '.tr' = 'application/x-troff' + '.trm' = 'application/x-msterminal' + '.trx' = 'application/xml' + '.ts' = 'video/vnd.dlna.mpeg-tts' + '.tsv' = 'text/tab-separated-values' + '.ttf' = 'application/font-sfnt' + '.tts' = 'video/vnd.dlna.mpeg-tts' + '.txt' = 'text/plain' + '.u32' = 'application/octet-stream' + '.uls' = 'text/iuls' + '.user' = 'text/plain' + '.ustar' = 'application/x-ustar' + '.vb' = 'text/plain' + '.vbdproj' = 'text/plain' + '.vbk' = 'video/mpeg' + '.vbproj' = 'text/plain' + '.vbs' = 'text/vbscript' + '.vcf' = 'text/x-vcard' + '.vcproj' = 'application/xml' + '.vcs' = 'text/plain' + '.vcxproj' = 'application/xml' + '.vddproj' = 'text/plain' + '.vdp' = 'text/plain' + '.vdproj' = 'text/plain' + '.vdx' = 'application/vnd.ms-visio.viewer' + '.vml' = 'text/xml' + '.vscontent' = 'application/xml' + '.vsct' = 'text/xml' + '.vsd' = 'application/vnd.visio' + '.vsi' = 'application/ms-vsi' + '.vsix' = 'application/vsix' + '.vsixlangpack' = 'text/xml' + '.vsixmanifest' = 'text/xml' + '.vsmdi' = 'application/xml' + '.vspscc' = 'text/plain' + '.vss' = 'application/vnd.visio' + '.vsscc' = 'text/plain' + '.vssettings' = 'text/xml' + '.vssscc' = 'text/plain' + '.vst' = 'application/vnd.visio' + '.vstemplate' = 'text/xml' + '.vsto' = 'application/x-ms-vsto' + '.vsw' = 'application/vnd.visio' + '.vsx' = 'application/vnd.visio' + '.vtx' = 'application/vnd.visio' + '.wasm' = 'application/wasm' + '.wav' = 'audio/wav' + '.wave' = 'audio/wav' + '.wax' = 'audio/x-ms-wax' + '.wbk' = 'application/msword' + '.wbmp' = 'image/vnd.wap.wbmp' + '.wcm' = 'application/vnd.ms-works' + '.wdb' = 'application/vnd.ms-works' + '.wdp' = 'image/vnd.ms-photo' + '.webarchive' = 'application/x-safari-webarchive' + '.webm' = 'video/webm' + '.webp' = 'image/webp' + '.webtest' = 'application/xml' + '.wiq' = 'application/xml' + '.wiz' = 'application/msword' + '.wks' = 'application/vnd.ms-works' + '.wlmp' = 'application/wlmoviemaker' + '.wlpginstall' = 'application/x-wlpg-detect' + '.wlpginstall3' = 'application/x-wlpg3-detect' + '.wm' = 'video/x-ms-wm' + '.wma' = 'audio/x-ms-wma' + '.wmd' = 'application/x-ms-wmd' + '.wmf' = 'application/x-msmetafile' + '.wml' = 'text/vnd.wap.wml' + '.wmlc' = 'application/vnd.wap.wmlc' + '.wmls' = 'text/vnd.wap.wmlscript' + '.wmlsc' = 'application/vnd.wap.wmlscriptc' + '.wmp' = 'video/x-ms-wmp' + '.wmv' = 'video/x-ms-wmv' + '.wmx' = 'video/x-ms-wmx' + '.wmz' = 'application/x-ms-wmz' + '.woff' = 'application/font-woff' + '.woff2' = 'application/font-woff2' + '.wpl' = 'application/vnd.ms-wpl' + '.wps' = 'application/vnd.ms-works' + '.wri' = 'application/x-mswrite' + '.wrl' = 'x-world/x-vrml' + '.wrz' = 'x-world/x-vrml' + '.wsc' = 'text/scriptlet' + '.wsdl' = 'text/xml' + '.wvx' = 'video/x-ms-wvx' + '.x' = 'application/directx' + '.xaf' = 'x-world/x-vrml' + '.xaml' = 'application/xaml+xml' + '.xap' = 'application/x-silverlight-app' + '.xbap' = 'application/x-ms-xbap' + '.xbm' = 'image/x-xbitmap' + '.xdr' = 'text/plain' + '.xht' = 'application/xhtml+xml' + '.xhtml' = 'application/xhtml+xml' + '.xla' = 'application/vnd.ms-excel' + '.xlam' = 'application/vnd.ms-excel.addin.macroEnabled.12' + '.xlc' = 'application/vnd.ms-excel' + '.xld' = 'application/vnd.ms-excel' + '.xlk' = 'application/vnd.ms-excel' + '.xll' = 'application/vnd.ms-excel' + '.xlm' = 'application/vnd.ms-excel' + '.xls' = 'application/vnd.ms-excel' + '.xlsb' = 'application/vnd.ms-excel.sheet.binary.macroEnabled.12' + '.xlsm' = 'application/vnd.ms-excel.sheet.macroEnabled.12' + '.xlsx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + '.xlt' = 'application/vnd.ms-excel' + '.xltm' = 'application/vnd.ms-excel.template.macroEnabled.12' + '.xltx' = 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' + '.xlw' = 'application/vnd.ms-excel' + '.xml' = 'text/xml' + '.xmp' = 'application/octet-stream' + '.xmta' = 'application/xml' + '.xof' = 'x-world/x-vrml' + '.xoml' = 'text/plain' + '.xpm' = 'image/x-xpixmap' + '.xps' = 'application/vnd.ms-xpsdocument' + '.xrm-ms' = 'text/xml' + '.xsc' = 'application/xml' + '.xsd' = 'text/xml' + '.xsf' = 'text/xml' + '.xsl' = 'text/xml' + '.xslt' = 'text/xml' + '.xsn' = 'application/octet-stream' + '.xss' = 'application/xml' + '.xspf' = 'application/xspf+xml' + '.xtp' = 'application/octet-stream' + '.xwd' = 'image/x-xwindowdump' + '.yaml' = 'application/x-yaml' + '.yml' = 'application/x-yaml' + '.z' = 'application/x-compress' + '.zip' = 'application/zip' + } } + It "Returns correct content type for <_>" -ForEach ($types.Keys) { + Get-PodeContentType -Extension $_ | Should -Be $types[$_] - $types.Keys | ForEach-Object { - It "Return correct type for $($_)" { - Get-PodeContentType -Extension $_ | Should Be $types[$_] - } } + } } Describe 'Get-PodeStatusDescription' { It 'Returns no description for no StatusCode' { - Get-PodeStatusDescription | Should Be ([string]::Empty) + Get-PodeStatusDescription | Should -Be ([string]::Empty) } It 'Returns no description for unknown StatusCode' { - Get-PodeStatusDescription -StatusCode 9001 | Should Be ([string]::Empty) + Get-PodeStatusDescription -StatusCode 9001 | Should -Be ([string]::Empty) } It 'Returns description for StatusCode' { - Get-PodeStatusDescription -StatusCode 404 | Should Be 'Not Found' + Get-PodeStatusDescription -StatusCode 404 | Should -Be 'Not Found' } It 'Returns description for first StatusCode' { - Get-PodeStatusDescription -StatusCode 100 | Should Be 'Continue' + Get-PodeStatusDescription -StatusCode 100 | Should -Be 'Continue' } It 'Returns description for last StatusCode' { - Get-PodeStatusDescription -StatusCode 526 | Should Be 'Invalid SSL Certificate' + Get-PodeStatusDescription -StatusCode 526 | Should -Be 'Invalid SSL Certificate' } - Context 'All Extension Loop' { - $codes = @{ - '100' = 'Continue'; - '101' = 'Switching Protocols'; - '102' = 'Processing'; - '103' = 'Early Hints'; - '200' = 'OK'; - '201' = 'Created'; - '202' = 'Accepted'; - '203' = 'Non-Authoritative Information'; - '204' = 'No Content'; - '205' = 'Reset Content'; - '206' = 'Partial Content'; - '207' = 'Multi-Status'; - '208' = 'Already Reported'; - '226' = 'IM Used'; - '300' = 'Multiple Choices'; - '301' = 'Moved Permanently'; - '302' = 'Found'; - '303' = 'See Other'; - '304' = 'Not Modified'; - '305' = 'Use Proxy'; - '306' = 'Switch Proxy'; - '307' = 'Temporary Redirect'; - '308' = 'Permanent Redirect'; - '400' = 'Bad Request'; - '401' = 'Unauthorized'; - '402' = 'Payment Required'; - '403' = 'Forbidden'; - '404' = 'Not Found'; - '405' = 'Method Not Allowed'; - '406' = 'Not Acceptable'; - '407' = 'Proxy Authentication Required'; - '408' = 'Request Timeout'; - '409' = 'Conflict'; - '410' = 'Gone'; - '411' = 'Length Required'; - '412' = 'Precondition Failed'; - '413' = 'Payload Too Large'; - '414' = 'URI Too Long'; - '415' = 'Unsupported Media Type'; - '416' = 'Range Not Satisfiable'; - '417' = 'Expectation Failed'; - '418' = "I'm a Teapot"; - '419' = 'Page Expired'; - '420' = 'Enhance Your Calm'; - '421' = 'Misdirected Request'; - '422' = 'Unprocessable Entity'; - '423' = 'Locked'; - '424' = 'Failed Dependency'; - '426' = 'Upgrade Required'; - '428' = 'Precondition Required'; - '429' = 'Too Many Requests'; - '431' = 'Request Header Fields Too Large'; - '440' = 'Login Time-out'; - '450' = 'Blocked by Windows Parental Controls'; - '451' = 'Unavailable For Legal Reasons'; - '500' = 'Internal Server Error'; - '501' = 'Not Implemented'; - '502' = 'Bad Gateway'; - '503' = 'Service Unavailable'; - '504' = 'Gateway Timeout'; - '505' = 'HTTP Version Not Supported'; - '506' = 'Variant Also Negotiates'; - '507' = 'Insufficient Storage'; - '508' = 'Loop Detected'; - '510' = 'Not Extended'; - '511' = 'Network Authentication Required'; - '526' = 'Invalid SSL Certificate'; + BeforeAll { + $codes = @{ + '100' = 'Continue' + '101' = 'Switching Protocols' + '102' = 'Processing' + '103' = 'Early Hints' + '200' = 'OK' + '201' = 'Created' + '202' = 'Accepted' + '203' = 'Non-Authoritative Information' + '204' = 'No Content' + '205' = 'Reset Content' + '206' = 'Partial Content' + '207' = 'Multi-Status' + '208' = 'Already Reported' + '226' = 'IM Used' + '300' = 'Multiple Choices' + '301' = 'Moved Permanently' + '302' = 'Found' + '303' = 'See Other' + '304' = 'Not Modified' + '305' = 'Use Proxy' + '306' = 'Switch Proxy' + '307' = 'Temporary Redirect' + '308' = 'Permanent Redirect' + '400' = 'Bad Request' + '401' = 'Unauthorized' + '402' = 'Payment Required' + '403' = 'Forbidden' + '404' = 'Not Found' + '405' = 'Method Not Allowed' + '406' = 'Not Acceptable' + '407' = 'Proxy Authentication Required' + '408' = 'Request Timeout' + '409' = 'Conflict' + '410' = 'Gone' + '411' = 'Length Required' + '412' = 'Precondition Failed' + '413' = 'Payload Too Large' + '414' = 'URI Too Long' + '415' = 'Unsupported Media Type' + '416' = 'Range Not Satisfiable' + '417' = 'Expectation Failed' + '418' = "I'm a Teapot" + '419' = 'Page Expired' + '420' = 'Enhance Your Calm' + '421' = 'Misdirected Request' + '422' = 'Unprocessable Entity' + '423' = 'Locked' + '424' = 'Failed Dependency' + '426' = 'Upgrade Required' + '428' = 'Precondition Required' + '429' = 'Too Many Requests' + '431' = 'Request Header Fields Too Large' + '440' = 'Login Time-out' + '450' = 'Blocked by Windows Parental Controls' + '451' = 'Unavailable For Legal Reasons' + '500' = 'Internal Server Error' + '501' = 'Not Implemented' + '502' = 'Bad Gateway' + '503' = 'Service Unavailable' + '504' = 'Gateway Timeout' + '505' = 'HTTP Version Not Supported' + '506' = 'Variant Also Negotiates' + '507' = 'Insufficient Storage' + '508' = 'Loop Detected' + '510' = 'Not Extended' + '511' = 'Network Authentication Required' + '526' = 'Invalid SSL Certificate' + } } + BeforeDiscovery { + $codes = @{ + '100' = 'Continue' + '101' = 'Switching Protocols' + '102' = 'Processing' + '103' = 'Early Hints' + '200' = 'OK' + '201' = 'Created' + '202' = 'Accepted' + '203' = 'Non-Authoritative Information' + '204' = 'No Content' + '205' = 'Reset Content' + '206' = 'Partial Content' + '207' = 'Multi-Status' + '208' = 'Already Reported' + '226' = 'IM Used' + '300' = 'Multiple Choices' + '301' = 'Moved Permanently' + '302' = 'Found' + '303' = 'See Other' + '304' = 'Not Modified' + '305' = 'Use Proxy' + '306' = 'Switch Proxy' + '307' = 'Temporary Redirect' + '308' = 'Permanent Redirect' + '400' = 'Bad Request' + '401' = 'Unauthorized' + '402' = 'Payment Required' + '403' = 'Forbidden' + '404' = 'Not Found' + '405' = 'Method Not Allowed' + '406' = 'Not Acceptable' + '407' = 'Proxy Authentication Required' + '408' = 'Request Timeout' + '409' = 'Conflict' + '410' = 'Gone' + '411' = 'Length Required' + '412' = 'Precondition Failed' + '413' = 'Payload Too Large' + '414' = 'URI Too Long' + '415' = 'Unsupported Media Type' + '416' = 'Range Not Satisfiable' + '417' = 'Expectation Failed' + '418' = "I'm a Teapot" + '419' = 'Page Expired' + '420' = 'Enhance Your Calm' + '421' = 'Misdirected Request' + '422' = 'Unprocessable Entity' + '423' = 'Locked' + '424' = 'Failed Dependency' + '426' = 'Upgrade Required' + '428' = 'Precondition Required' + '429' = 'Too Many Requests' + '431' = 'Request Header Fields Too Large' + '440' = 'Login Time-out' + '450' = 'Blocked by Windows Parental Controls' + '451' = 'Unavailable For Legal Reasons' + '500' = 'Internal Server Error' + '501' = 'Not Implemented' + '502' = 'Bad Gateway' + '503' = 'Service Unavailable' + '504' = 'Gateway Timeout' + '505' = 'HTTP Version Not Supported' + '506' = 'Variant Also Negotiates' + '507' = 'Insufficient Storage' + '508' = 'Loop Detected' + '510' = 'Not Extended' + '511' = 'Network Authentication Required' + '526' = 'Invalid SSL Certificate' + } } + It "Returns description for the <_> StatusCode" -ForEach ($codes.Keys) { + Get-PodeStatusDescription -StatusCode $_ | Should -Be $codes[$_] } - $codes.Keys | ForEach-Object { - It "Returns description for the $($_) StatusCode" { - Get-PodeStatusDescription -StatusCode $_ | Should Be $codes[$_] - } - } - } + }#> } \ No newline at end of file diff --git a/tests/unit/Metrics.Tests.ps1 b/tests/unit/Metrics.Tests.ps1 index 8907b1b94..3430ac158 100644 --- a/tests/unit/Metrics.Tests.ps1 +++ b/tests/unit/Metrics.Tests.ps1 @@ -1,60 +1,63 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - -$PodeContext = @{ - Metrics = @{ - Server = @{ - StartTime = [datetime]::UtcNow - InitialLoadTime = [datetime]::UtcNow - RestartCount = 0 - } - Requests = @{ - Total = 10 - StatusCodes = @{ - '200' = 8 - '404' = 2 +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + + $PodeContext = @{ + Metrics = @{ + Server = @{ + StartTime = [datetime]::UtcNow + InitialLoadTime = [datetime]::UtcNow + RestartCount = 0 + } + Requests = @{ + Total = 10 + StatusCodes = @{ + '200' = 8 + '404' = 2 + } } } - } -} + } } Describe 'Get-PodeServerUptime' { It 'Returns the current session uptime' { $PodeContext.Metrics.Server.StartTime = ([datetime]::UtcNow.AddSeconds(-2)) - ((Get-PodeServerUptime) -ge 2000) | Should Be $true + ((Get-PodeServerUptime) -ge 2000) | Should -Be $true } It 'Returns the total uptime' { $PodeContext.Metrics.Server.InitialLoadTime = ([datetime]::UtcNow.AddSeconds(-2)) - ((Get-PodeServerUptime -Total) -ge 2000) | Should Be $true + ((Get-PodeServerUptime -Total) -ge 2000) | Should -Be $true } } Describe 'Get-PodeServerRestartCount' { It 'Returns the restart count' { $PodeContext.Metrics.Server.RestartCount = 1 - Get-PodeServerRestartCount | Should Be 1 + Get-PodeServerRestartCount | Should -Be 1 } } Describe 'Get-PodeServerRequestMetric' { It 'Returns the total number of requests' { - Get-PodeServerRequestMetric -Total | Should Be 10 + Get-PodeServerRequestMetric -Total | Should -Be 10 } It 'Returns each status code' { $codes = Get-PodeServerRequestMetric - $codes.Count | Should Be 2 - $codes['200'] | Should Be 8 - $codes['404'] | Should Be 2 + $codes.Count | Should -Be 2 + $codes['200'] | Should -Be 8 + $codes['404'] | Should -Be 2 } It 'Returns total request that resulted in a 200' { - Get-PodeServerRequestMetric -StatusCode 200 | Should Be 8 + Get-PodeServerRequestMetric -StatusCode 200 | Should -Be 8 } It 'Returns 0 requests that resulted in a 201' { - Get-PodeServerRequestMetric -StatusCode 201 | Should Be 0 + Get-PodeServerRequestMetric -StatusCode 201 | Should -Be 0 } } \ No newline at end of file diff --git a/tests/unit/Middleware.Tests.ps1 b/tests/unit/Middleware.Tests.ps1 index b608ab468..f4b7034d9 100644 --- a/tests/unit/Middleware.Tests.ps1 +++ b/tests/unit/Middleware.Tests.ps1 @@ -1,52 +1,61 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Get-PodeInbuiltMiddleware' { Context 'Invalid parameters supplied' { It 'Throws null name parameter error' { - { Get-PodeInbuiltMiddleware -Name $null -ScriptBlock {} } | Should Throw 'null or empty' + { Get-PodeInbuiltMiddleware -Name $null -ScriptBlock {} } | Should -Throw -ExpectedMessage '*null or empty*' } It 'Throws empty name parameter error' { - { Get-PodeInbuiltMiddleware -Name ([string]::Empty) -ScriptBlock {} } | Should Throw 'null or empty' + { Get-PodeInbuiltMiddleware -Name ([string]::Empty) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*null or empty*' } It 'Throws null logic error' { - { Get-PodeInbuiltMiddleware -Name 'test' -ScriptBlock $null } | Should Throw 'argument is null' + { Get-PodeInbuiltMiddleware -Name 'test' -ScriptBlock $null } | Should -Throw -ExpectedMessage '*argument is null*' } } Context 'Valid parameters' { It 'using default inbuilt logic' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @( - @{ 'Name' = $null; 'Logic' = { write-host 'pre1' } } - ); }; } + @{ 'Name' = $null; 'Logic' = { write-host 'pre1' } } + ) + } + } $logic = Get-PodeInbuiltMiddleware -Name '__pode_mw_access__' -ScriptBlock { write-host 'in1' } - $logic | Should Not Be $null - $logic.Name | Should Be '__pode_mw_access__' - $logic.Logic.ToString() | Should Be ({ write-host 'in1' }).ToString() + $logic | Should -Not -Be $null + $logic.Name | Should -Be '__pode_mw_access__' + $logic.Logic.ToString() | Should -Be ({ write-host 'in1' }).ToString() - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic | Should Be ({ write-host 'pre1' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic | Should -Be ({ write-host 'pre1' }).ToString() } It 'using default override logic' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @( - @{ 'Name' = $null; 'Logic' = { write-host 'pre1' } }; - @{ 'Name' = '__pode_mw_access__'; 'Logic' = { write-host 'over1' } } - ); }; } + @{ 'Name' = $null; 'Logic' = { write-host 'pre1' } } + @{ 'Name' = '__pode_mw_access__'; 'Logic' = { write-host 'over1' } } + ) + } + } $logic = Get-PodeInbuiltMiddleware -Name '__pode_mw_access__' -ScriptBlock { write-host 'in1' } - $logic | Should Not Be $null - $logic.Name | Should Be '__pode_mw_access__' - $logic.Logic.ToString() | Should Be ({ write-host 'over1' }).ToString() + $logic | Should -Not -Be $null + $logic.Name | Should -Be '__pode_mw_access__' + $logic.Logic.ToString() | Should -Be ({ write-host 'over1' }).ToString() - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic | Should Be ({ write-host 'pre1' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic | Should -Be ({ write-host 'pre1' }).ToString() } } } @@ -58,8 +67,8 @@ Describe 'Middleware' { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle1' } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() } It 'Adds single middleware script to list with route' { @@ -67,9 +76,9 @@ Describe 'Middleware' { Add-PodeMiddleware -Name 'Test1' -Route '/api' -ScriptBlock { write-host 'middle1' } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[0].Route | Should Be '/api' + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[0].Route | Should -Be '/api' } It 'Adds two middleware scripts to list' { @@ -78,9 +87,9 @@ Describe 'Middleware' { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle1' } Add-PodeMiddleware -Name 'Test2' -ScriptBlock { write-host 'middle2' } - $PodeContext.Server.Middleware.Length | Should Be 2 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[1].Logic.ToString() | Should Be ({ Write-Host 'middle2' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 2 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[1].Logic.ToString() | Should -Be ({ Write-Host 'middle2' }).ToString() } It 'Adds middleware script to override inbuilt ones' { @@ -88,21 +97,21 @@ Describe 'Middleware' { Add-PodeMiddleware -Name '__pode_mw_access__' -ScriptBlock { write-host 'middle1' } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[0].Name | Should Be '__pode_mw_access__' + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[0].Name | Should -Be '__pode_mw_access__' } It 'Throws error when adding middleware script with duplicate name' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @(); }; } Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle1' } - { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle2' } } | Should Throw 'already defined' + { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle2' } } | Should -Throw -ExpectedMessage '*already defined*' } It 'Throws error when adding middleware hash with no logic' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @(); }; } - { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Rand' = { write-host 'middle1' } } } | Should Throw 'no logic supplied' + { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Rand' = { write-host 'middle1' } } } | Should -Throw -ExpectedMessage '*no logic supplied*' } It 'Adds single middleware hash to list' { @@ -110,8 +119,8 @@ Describe 'Middleware' { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle1' } } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() } It 'Adds single middleware hash to list with route' { @@ -119,9 +128,9 @@ Describe 'Middleware' { Add-PodeMiddleware -Name 'Test1' -Route '/api' -InputObject @{ 'Logic' = { write-host 'middle1' } } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[0].Route | Should Be '/api' + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[0].Route | Should -Be '/api' } It 'Adds two middleware hashs to list' { @@ -130,9 +139,9 @@ Describe 'Middleware' { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle1' } } Add-PodeMiddleware -Name 'Test2' -InputObject @{ 'Logic' = { write-host 'middle2' } } - $PodeContext.Server.Middleware.Length | Should Be 2 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[1].Logic.ToString() | Should Be ({ Write-Host 'middle2' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 2 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[1].Logic.ToString() | Should -Be ({ Write-Host 'middle2' }).ToString() } It 'Adds middleware hash to override inbuilt ones' { @@ -140,34 +149,34 @@ Describe 'Middleware' { Add-PodeMiddleware -Name '__pode_mw_access__' -InputObject @{ 'Logic' = { write-host 'middle1' } } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[0].Name | Should Be '__pode_mw_access__' + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[0].Name | Should -Be '__pode_mw_access__' } It 'Throws error when adding middleware hash with duplicate name' { $PodeContext = @{ 'Server' = @{ 'Middleware' = @(); }; } Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle1' } } - { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle2' } } } | Should Throw 'already defined' + { Add-PodeMiddleware -Name 'Test1' -InputObject @{ 'Logic' = { write-host 'middle2' } } } | Should -Throw -ExpectedMessage '*already defined*' } } } Describe 'Invoke-PodeMiddleware' { It 'Returns true for no middleware' { - (Invoke-PodeMiddleware -Middleware @()) | Should Be $true + (Invoke-PodeMiddleware -Middleware @()) | Should -Be $true } It 'Runs the logic for a single middleware and returns true' { Mock Invoke-PodeScriptBlock { return $true } $WebEvent = @{ 'Middleware' = @{} } $midware = @{ - 'Options' = @{}; - 'Logic' = { 'test' | Out-Null }; + 'Options' = @{} + 'Logic' = { 'test' | Out-Null } } - Invoke-PodeMiddleware -Middleware @($midware) | Should Be $true + Invoke-PodeMiddleware -Middleware @($midware) | Should -Be $true Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It } @@ -176,12 +185,12 @@ Describe 'Invoke-PodeMiddleware' { Mock Invoke-PodeScriptBlock { return $true } $WebEvent = @{ 'Middleware' = @{} } $midware = @{ - 'Options' = @{}; - 'Route' = '/'; - 'Logic' = { 'test' | Out-Null }; + 'Options' = @{} + 'Route' = '/' + 'Logic' = { 'test' | Out-Null } } - Invoke-PodeMiddleware -Middleware @($midware) -Route '/' | Should Be $true + Invoke-PodeMiddleware -Middleware @($midware) -Route '/' | Should -Be $true Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It } @@ -191,16 +200,16 @@ Describe 'Invoke-PodeMiddleware' { $WebEvent = @{ 'Middleware' = @{} } $midware1 = @{ - 'Options' = @{}; - 'Logic' = { 'test' | Out-Null }; + 'Options' = @{} + 'Logic' = { 'test' | Out-Null } } $midware2 = @{ - 'Options' = @{}; - 'Logic' = { 'test2' | Out-Null }; + 'Options' = @{} + 'Logic' = { 'test2' | Out-Null } } - Invoke-PodeMiddleware -Middleware @($midware1, $midware2) | Should Be $true + Invoke-PodeMiddleware -Middleware @($midware1, $midware2) | Should -Be $true Assert-MockCalled Invoke-PodeScriptBlock -Times 2 -Scope It } @@ -209,11 +218,11 @@ Describe 'Invoke-PodeMiddleware' { Mock Invoke-PodeScriptBlock { return $false } $WebEvent = @{ 'Middleware' = @{} } $midware = @{ - 'Options' = @{}; - 'Logic' = { 'test' | Out-Null }; + 'Options' = @{} + 'Logic' = { 'test' | Out-Null } } - Invoke-PodeMiddleware -Middleware @($midware) | Should Be $false + Invoke-PodeMiddleware -Middleware @($midware) | Should -Be $false Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It } @@ -225,11 +234,11 @@ Describe 'Invoke-PodeMiddleware' { $WebEvent = @{ 'Middleware' = @{} } $midware = @{ - 'Options' = @{}; - 'Logic' = { 'test' | Out-Null }; + 'Options' = @{} + 'Logic' = { 'test' | Out-Null } } - Invoke-PodeMiddleware -Middleware @($midware) | Should Be $false + Invoke-PodeMiddleware -Middleware @($midware) | Should -Be $false Assert-MockCalled Invoke-PodeScriptBlock -Times 1 -Scope It Assert-MockCalled Set-PodeResponseStatus -Times 1 -Scope It @@ -238,14 +247,14 @@ Describe 'Invoke-PodeMiddleware' { Describe 'Get-PodeAccessMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock and invokes it as true' { $r = Get-PodeAccessMiddleware - $r.Name | Should Be '__pode_mw_access__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_access__' + $r.Logic | Should -Not -Be $null Mock Test-PodeIPAccess { return $true } @@ -253,19 +262,19 @@ Describe 'Get-PodeAccessMiddleware' { 'Request' = @{ 'RemoteEndPoint' = @{ 'Address' = 'localhost' } } } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as true, no rule' { $r = Get-PodeAccessMiddleware - $r.Name | Should Be '__pode_mw_access__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_access__' + $r.Logic | Should -Not -Be $null $WebEvent = @{ 'Request' = @{ 'RemoteEndPoint' = @{ 'Address' = 'localhost' } } } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as false' { @@ -278,8 +287,8 @@ Describe 'Get-PodeAccessMiddleware' { } $r = Get-PodeAccessMiddleware - $r.Name | Should Be '__pode_mw_access__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_access__' + $r.Logic | Should -Not -Be $null Mock Test-PodeIPAccess { return $false } Mock Set-PodeResponseStatus { } @@ -288,20 +297,20 @@ Describe 'Get-PodeAccessMiddleware' { 'Request' = @{ 'RemoteEndPoint' = @{ 'Address' = 'localhost' } } } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false } } Describe 'Get-PodeLimitMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock and invokes it as true' { $r = Get-PodeLimitMiddleware - $r.Name | Should Be '__pode_mw_rate_limit__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_rate_limit__' + $r.Logic | Should -Not -Be $null Mock Test-PodeIPLimit { return $true } @@ -309,19 +318,19 @@ Describe 'Get-PodeLimitMiddleware' { 'Request' = @{ 'RemoteEndPoint' = @{ 'Address' = 'localhost' } } } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as true, no rules' { $r = Get-PodeLimitMiddleware - $r.Name | Should Be '__pode_mw_rate_limit__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_rate_limit__' + $r.Logic | Should -Not -Be $null $WebEvent = @{ 'Request' = @{ 'RemoteEndPoint' = @{ 'Address' = 'localhost' } } } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as false' { @@ -334,8 +343,8 @@ Describe 'Get-PodeLimitMiddleware' { } $r = Get-PodeLimitMiddleware - $r.Name | Should Be '__pode_mw_rate_limit__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_rate_limit__' + $r.Logic | Should -Not -Be $null Mock Test-PodeIPLimit { return $false } Mock Set-PodeResponseStatus { } @@ -344,238 +353,246 @@ Describe 'Get-PodeLimitMiddleware' { 'Request' = @{ 'RemoteEndPoint' = @{ 'Address' = 'localhost' } } } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false } } Describe 'Get-PodeRouteValidateMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock and invokes it as true' { $WebEvent = @{ 'Parameters' = @{} } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null Mock Find-PodeStaticRoute { return $null } Mock Find-PodeRoute { return @{ 'Parameters' = @{}; 'Logic' = { Write-Host 'hello' }; } } $WebEvent = @{ - 'Method' = 'GET'; - 'Path' = '/'; + 'Method' = 'GET' + 'Path' = '/' } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as true, overriding the content type' { $WebEvent = @{ - Parameters = @{}; + Parameters = @{} ContentType = 'text/plain' - Method = 'GET' - Path = '/' + Method = 'GET' + Path = '/' } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null Mock Find-PodeStaticRoute { return $null } Mock Find-PodeRoute { return @{ - 'Parameters' = @{}; - 'Logic' = { Write-Host 'hello' }; - 'ContentType' = 'application/json'; - } } + 'Parameters' = @{} + 'Logic' = { Write-Host 'hello' } + 'ContentType' = 'application/json' + } } - (. $r.Logic) | Should Be $true - $WebEvent.Route | Should Not Be $null - $WebEvent.ContentType | Should Be 'application/json' + (. $r.Logic) | Should -Be $true + $WebEvent.Route | Should -Not -Be $null + $WebEvent.ContentType | Should -Be 'application/json' } It 'Returns a ScriptBlock and invokes it as false' { $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null Mock Find-PodeStaticRoute { return $null } Mock Find-PodeRoute { return $null } Mock Set-PodeResponseStatus { } $WebEvent = @{ - 'Method' = 'GET'; - 'Path' = '/'; + 'Method' = 'GET' + 'Path' = '/' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false } It 'Returns a ScriptBlock, invokes false for no static path' { $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null Mock Find-PodeStaticRoute { return $null } Mock Find-PodeRoute { return $null } Mock Set-PodeResponseStatus { } $WebEvent = @{ - 'Method' = 'GET'; - 'Path' = '/'; + 'Method' = 'GET' + 'Path' = '/' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false } It 'Returns a ScriptBlock and invokes it as true, for static content' { $WebEvent = @{ - Parameters = @{}; + Parameters = @{} ContentType = 'text/plain' - Method = 'GET' - Path = '/' + Method = 'GET' + Path = '/' } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null Mock Find-PodeStaticRoute { return @{ Content = @{ Source = '/'; Download = $true }; Route = @{} } } Mock Find-PodeRoute { return $null } - (. $r.Logic) | Should Be $true - $WebEvent.Route | Should Not Be $null - $WebEvent.StaticContent | Should Not Be $null - $WebEvent.StaticContent.Download | Should Be $true + (. $r.Logic) | Should -Be $true + $WebEvent.Route | Should -Not -Be $null + $WebEvent.StaticContent | Should -Not -Be $null + $WebEvent.StaticContent.Download | Should -Be $true } It 'Returns a ScriptBlock, invokes false for static path, with no caching' { $WebEvent = @{ - Parameters = @{}; + Parameters = @{} ContentType = 'text/plain' - Method = 'GET' - Path = '/' + Method = 'GET' + Path = '/' } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $false + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $false + } + } } - }} - }} + } + } Mock Find-PodeStaticRoute { return @{ Content = @{ Source = '/' }; Route = @{} } } Mock Find-PodeRoute { return $null } - (. $r.Logic) | Should Be $true - $WebEvent.Route | Should Not Be $null - $WebEvent.StaticContent | Should Not Be $null + (. $r.Logic) | Should -Be $true + $WebEvent.Route | Should -Not -Be $null + $WebEvent.StaticContent | Should -Not -Be $null } It 'Returns a ScriptBlock, invokes false for static path, with no caching from exclude' { $WebEvent = @{ - Parameters = @{}; + Parameters = @{} ContentType = 'text/plain' - Method = 'GET' - Path = '/' + Method = 'GET' + Path = '/' } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $true; - 'Exclude' = '/' + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $true + 'Exclude' = '/' + } + } } - }} - }} + } + } Mock Find-PodeStaticRoute { return @{ Content = @{ Source = '/' }; Route = @{} } } Mock Find-PodeRoute { return $null } - (. $r.Logic) | Should Be $true - $WebEvent.Route | Should Not Be $null - $WebEvent.StaticContent | Should Not Be $null + (. $r.Logic) | Should -Be $true + $WebEvent.Route | Should -Not -Be $null + $WebEvent.StaticContent | Should -Not -Be $null } It 'Returns a ScriptBlock, invokes false for static path, with no caching from include' { $WebEvent = @{ - Parameters = @{}; + Parameters = @{} ContentType = 'text/plain' - Method = 'GET' - Path = '/' + Method = 'GET' + Path = '/' } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $true; - 'Include' = '/route' + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $true + 'Include' = '/route' + } + } } - }} - }} + } + } Mock Find-PodeStaticRoute { return @{ Content = @{ Source = '/' }; Route = @{} } } Mock Find-PodeRoute { return $null } - (. $r.Logic) | Should Be $true - $WebEvent.Route | Should Not Be $null - $WebEvent.StaticContent | Should Not Be $null + (. $r.Logic) | Should -Be $true + $WebEvent.Route | Should -Not -Be $null + $WebEvent.StaticContent | Should -Not -Be $null } It 'Returns a ScriptBlock, invokes false for static path, with caching' { $WebEvent = @{ - Parameters = @{}; + Parameters = @{} ContentType = 'text/plain' - Method = 'GET' - Path = '/' + Method = 'GET' + Path = '/' } $r = Get-PodeRouteValidateMiddleware - $r.Name | Should Be '__pode_mw_route_validation__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_route_validation__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $true; + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $true + } + } } - }} - }} + } + } Mock Find-PodeStaticRoute { return @{ Content = @{ Source = '/' }; Route = @{} } } Mock Find-PodeRoute { return $null } - (. $r.Logic) | Should Be $true - $WebEvent.Route | Should Not Be $null - $WebEvent.StaticContent | Should Not Be $null + (. $r.Logic) | Should -Be $true + $WebEvent.Route | Should -Not -Be $null + $WebEvent.StaticContent | Should -Not -Be $null } } Describe 'Get-PodeBodyMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock and invokes it as true' { $r = Get-PodeBodyMiddleware - $r.Name | Should Be '__pode_mw_body_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_body_parsing__' + $r.Logic | Should -Not -Be $null Mock ConvertFrom-PodeRequestContent { return @{ 'Data' = @{}; 'Files' = @{}; } } @@ -583,13 +600,13 @@ Describe 'Get-PodeBodyMiddleware' { 'Request' = 'value' } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as false' { $r = Get-PodeBodyMiddleware - $r.Name | Should Be '__pode_mw_body_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_body_parsing__' + $r.Logic | Should -Not -Be $null Mock ConvertFrom-PodeRequestContent { throw 'error' } Mock Set-PodeResponseStatus { } @@ -598,20 +615,20 @@ Describe 'Get-PodeBodyMiddleware' { 'Request' = 'value' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false } } Describe 'Get-PodeQueryMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock and invokes it as true' { $r = Get-PodeQueryMiddleware - $r.Name | Should Be '__pode_mw_query_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_query_parsing__' + $r.Logic | Should -Not -Be $null Mock ConvertFrom-PodeNameValueToHashTable { return 'string' } @@ -619,13 +636,13 @@ Describe 'Get-PodeQueryMiddleware' { 'Request' = @{ 'QueryString' = [System.Web.HttpUtility]::ParseQueryString('name=bob') } } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock and invokes it as false' { $r = Get-PodeQueryMiddleware - $r.Name | Should Be '__pode_mw_query_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_query_parsing__' + $r.Logic | Should -Not -Be $null Mock ConvertFrom-PodeNameValueToHashTable { throw 'error' } Mock Set-PodeResponseStatus { } @@ -634,149 +651,158 @@ Describe 'Get-PodeQueryMiddleware' { 'Request' = @{ 'QueryString' = 'name=bob' } } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false } } Describe 'Get-PodePublicMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock, invokes true for no static path' { $r = Get-PodePublicMiddleware - $r.Name | Should Be '__pode_mw_static_content__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_static_content__' + $r.Logic | Should -Not -Be $null Mock Find-PodePublicRoute { return $null } $WebEvent = @{ - 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = ''; + 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = '' } - (. $r.Logic) | Should Be $true + (. $r.Logic) | Should -Be $true } It 'Returns a ScriptBlock, invokes false for static path' { $r = Get-PodePublicMiddleware - $r.Name | Should Be '__pode_mw_static_content__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_static_content__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ } } - }} + 'Web' = @{ 'Static' = @{ } } + } + } Mock Find-PodePublicRoute { return '/' } Mock Write-PodeFileResponse { } $WebEvent = @{ - 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = ''; + 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = '' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It } It 'Returns a ScriptBlock, invokes false for static path, with no caching' { $r = Get-PodePublicMiddleware - $r.Name | Should Be '__pode_mw_static_content__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_static_content__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $false + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $false + } + } } - }} - }} + } + } Mock Find-PodePublicRoute { return '/' } Mock Write-PodeFileResponse { } $WebEvent = @{ - 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = ''; + 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = '' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It } It 'Returns a ScriptBlock, invokes false for static path, with no caching from exclude' { $r = Get-PodePublicMiddleware - $r.Name | Should Be '__pode_mw_static_content__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_static_content__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $true; - 'Exclude' = '/' + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $true + 'Exclude' = '/' + } + } } - }} - }} + } + } Mock Find-PodePublicRoute { return '/' } Mock Write-PodeFileResponse { } $WebEvent = @{ - 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = ''; + 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = '' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It } It 'Returns a ScriptBlock, invokes false for static path, with no caching from include' { $r = Get-PodePublicMiddleware - $r.Name | Should Be '__pode_mw_static_content__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_static_content__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $true; - 'Include' = '/route' + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $true + 'Include' = '/route' + } + } } - }} - }} + } + } Mock Find-PodePublicRoute { return '/' } Mock Write-PodeFileResponse { } $WebEvent = @{ - 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = ''; + 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = '' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It } It 'Returns a ScriptBlock, invokes false for static path, with caching' { $r = Get-PodePublicMiddleware - $r.Name | Should Be '__pode_mw_static_content__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_static_content__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ - 'Web' = @{ 'Static' = @{ - 'Cache' = @{ - 'Enabled' = $true; + 'Web' = @{ 'Static' = @{ + 'Cache' = @{ + 'Enabled' = $true + } + } } - }} - }} + } + } Mock Find-PodePublicRoute { return '/' } Mock Write-PodeFileResponse { } $WebEvent = @{ - 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = ''; + 'Path' = '/'; 'Protocol' = 'http'; 'Endpoint' = '' } - (. $r.Logic) | Should Be $false + (. $r.Logic) | Should -Be $false Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It } @@ -784,58 +810,58 @@ Describe 'Get-PodePublicMiddleware' { Describe 'Get-PodeCookieMiddleware' { Mock Get-PodeInbuiltMiddleware { return @{ - 'Name' = $Name; - 'Logic' = $ScriptBlock; - } } + 'Name' = $Name + 'Logic' = $ScriptBlock + } } It 'Returns a ScriptBlock, invokes true for not being serverless' { $r = Get-PodeCookieMiddleware - $r.Name | Should Be '__pode_mw_cookie_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_cookie_parsing__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ 'IsServerless' = $false } } - (. $r.Logic @{}) | Should Be $true + (. $r.Logic @{}) | Should -Be $true } It 'Returns a ScriptBlock, invokes true for cookies already being set' { $r = Get-PodeCookieMiddleware - $r.Name | Should Be '__pode_mw_cookie_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_cookie_parsing__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } (. $r.Logic @{ - 'Cookies' = @{ 'test' = 'value' }; - }) | Should Be $true + 'Cookies' = @{ 'test' = 'value' } + }) | Should -Be $true } It 'Returns a ScriptBlock, invokes true for for no cookies on header' { $r = Get-PodeCookieMiddleware - $r.Name | Should Be '__pode_mw_cookie_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_cookie_parsing__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } Mock Get-PodeHeader { return $null } (. $r.Logic @{ - 'Cookies' = @{}; - }) | Should Be $true + 'Cookies' = @{} + }) | Should -Be $true } It 'Returns a ScriptBlock, invokes true and parses cookies' { $r = Get-PodeCookieMiddleware - $r.Name | Should Be '__pode_mw_cookie_parsing__' - $r.Logic | Should Not Be $null + $r.Name | Should -Be '__pode_mw_cookie_parsing__' + $r.Logic | Should -Not -Be $null $PodeContext = @{ 'Server' = @{ 'IsServerless' = $true } } Mock Get-PodeHeader { return 'key1=value1; key2=value2' } $WebEvent = @{ 'Cookies' = @{} } - (. $r.Logic $WebEvent) | Should Be $true + (. $r.Logic $WebEvent) | Should -Be $true - $WebEvent.Cookies.Count | Should Be 2 - $WebEvent.Cookies['key1'].Value | Should Be 'value1' - $WebEvent.Cookies['key2'].Value | Should Be 'value2' + $WebEvent.Cookies.Count | Should -Be 2 + $WebEvent.Cookies['key1'].Value | Should -Be 'value1' + $WebEvent.Cookies['key2'].Value | Should -Be 'value2' } } @@ -845,12 +871,12 @@ Describe 'Remove-PodeMiddleware' { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle1' } - $PodeContext.Server.Middleware.Length | Should Be 1 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 1 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() Remove-PodeMiddleware -Name 'Test1' - $PodeContext.Server.Middleware.Length | Should Be 0 + $PodeContext.Server.Middleware.Length | Should -Be 0 } } @@ -861,56 +887,60 @@ Describe 'Clear-PodeMiddleware' { Add-PodeMiddleware -Name 'Test1' -ScriptBlock { write-host 'middle1' } Add-PodeMiddleware -Name 'Test2' -ScriptBlock { write-host 'middle2' } - $PodeContext.Server.Middleware.Length | Should Be 2 - $PodeContext.Server.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $PodeContext.Server.Middleware[1].Logic.ToString() | Should Be ({ Write-Host 'middle2' }).ToString() + $PodeContext.Server.Middleware.Length | Should -Be 2 + $PodeContext.Server.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $PodeContext.Server.Middleware[1].Logic.ToString() | Should -Be ({ Write-Host 'middle2' }).ToString() Clear-PodeMiddleware - $PodeContext.Server.Middleware.Length | Should Be 0 + $PodeContext.Server.Middleware.Length | Should -Be 0 } } Describe 'Add-PodeBodyParser' { It 'Fails because a script is already defined' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } - { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should Not Throw - { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should Throw 'already a body parser' + { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should -Not -Throw + { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already a body parser*' } It 'Fails on an invalid content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } - { Add-PodeBodyParser -ContentType 'text_xml' -ScriptBlock {} } | Should Throw "Cannot validate argument on parameter 'ContentType'" + { Add-PodeBodyParser -ContentType 'text_xml' -ScriptBlock {} } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'ContentType'*" } It 'Adds a script for a content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } - { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should Not Throw - $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should Be $true + { Add-PodeBodyParser -ContentType 'text/xml' -ScriptBlock {} } | Should -Not -Throw + $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should -BeTrue } } Describe 'Remove-PodeBodyParser' { It 'Fails on an invalid content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{} } } - { Remove-PodeBodyParser -ContentType 'text_xml' } | Should Throw "Cannot validate argument on parameter 'ContentType'" + { Remove-PodeBodyParser -ContentType 'text_xml' } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'ContentType'*" } It 'Does nothing if no script set for content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{ - 'text/xml' = {} - } } } + 'text/xml' = {} + } + } + } - { Remove-PodeBodyParser -ContentType 'text/yaml' } | Should Not Throw - $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should Be $true + { Remove-PodeBodyParser -ContentType 'text/yaml' } | Should -Not -Throw + $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should -Be $true } It 'Removes the script for the content-type' { $PodeContext = @{ 'Server' = @{ 'BodyParsers' = @{ - 'text/xml' = {} - } } } + 'text/xml' = {} + } + } + } - { Remove-PodeBodyParser -ContentType 'text/xml' } | Should Not Throw - $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should Be $false + { Remove-PodeBodyParser -ContentType 'text/xml' } | Should -Not -Throw + $PodeContext.Server.BodyParsers.ContainsKey('text/xml') | Should -Be $false } } \ No newline at end of file diff --git a/tests/unit/NameGenerator.Tests.ps1 b/tests/unit/NameGenerator.Tests.ps1 index 2f8984ea1..140bf974c 100644 --- a/tests/unit/NameGenerator.Tests.ps1 +++ b/tests/unit/NameGenerator.Tests.ps1 @@ -1,11 +1,14 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Get-PodeRandomName' { - Mock 'Get-Random' { return 0 } + It 'Returns correct name' { - Get-PodeRandomName | Should Be 'admiring_almeida' + Mock 'Get-Random' { return 0 } + Get-PodeRandomName | Should -Be 'admiring_almeida' } } \ No newline at end of file diff --git a/tests/unit/Responses.Tests.ps1 b/tests/unit/Responses.Tests.ps1 index 39f4c1ea0..fb5dfef23 100644 --- a/tests/unit/Responses.Tests.ps1 +++ b/tests/unit/Responses.Tests.ps1 @@ -1,55 +1,63 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + +} Describe 'Set-PodeResponseStatus' { Context 'Valid values supplied' { - Mock 'Show-PodeErrorPage' { } - + BeforeEach { + Mock 'Show-PodeErrorPage' { } + } It 'Sets StatusCode only' { $WebEvent = @{ 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = '' } } Set-PodeResponseStatus -Code 418 - $WebEvent.Response.StatusCode | Should Be 418 - $WebEvent.Response.StatusDescription | Should Be "I'm a Teapot" - - Assert-MockCalled 'Show-PodeErrorPage' -Scope It -Times 1 + $WebEvent.Response.StatusCode | Should -Be 418 + $WebEvent.Response.StatusDescription | Should -Be "I'm a Teapot" + Should -Invoke Show-PodeErrorPage -Times 1 -Scope It + # Assert-MockCalled 'Show-PodeErrorPage' -Scope It -Times 1 } It 'Sets StatusCode and StatusDescription' { $WebEvent = @{ 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = '' } } Set-PodeResponseStatus -Code 418 -Description 'I am a Teapot' - $WebEvent.Response.StatusCode | Should Be 418 - $WebEvent.Response.StatusDescription | Should Be 'I am a Teapot' - - Assert-MockCalled 'Show-PodeErrorPage' -Scope It -Times 1 + $WebEvent.Response.StatusCode | Should -Be 418 + $WebEvent.Response.StatusDescription | Should -Be 'I am a Teapot' + Should -Invoke Show-PodeErrorPage -Times 1 -Scope It + #Assert-MockCalled 'Show-PodeErrorPage' -Scope It -Times 1 } It 'Sets 200 StatusCode' { $WebEvent = @{ 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = '' } } Set-PodeResponseStatus -Code 200 - $WebEvent.Response.StatusCode | Should Be 200 - $WebEvent.Response.StatusDescription | Should Be 'OK' - - Assert-MockCalled 'Show-PodeErrorPage' -Scope It -Times 0 + $WebEvent.Response.StatusCode | Should -Be 200 + $WebEvent.Response.StatusDescription | Should -Be 'OK' + Should -Invoke Show-PodeErrorPage -Times 0 -Scope It + # Assert-MockCalled 'Show-PodeErrorPage' -Scope It -Times 0 } } } Describe 'Move-PodeResponseUrl' { Context 'Valid values supplied' { - Mock Set-PodeHeader { $WebEvent.Response.Headers[$Name] = $Value } + BeforeEach { + Mock Set-PodeHeader { $WebEvent.Response.Headers[$Name] = $Value } } It 'Sets URL response for redirect' { $WebEvent = @{ 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Url 'https://google.com' - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'https://google.com' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'https://google.com' } It 'Sets URL response for moved' { @@ -57,143 +65,145 @@ Describe 'Move-PodeResponseUrl' { Move-PodeResponseUrl -Moved -Url 'https://google.com' - $WebEvent.Response.StatusCode | Should Be 301 - $WebEvent.Response.StatusDescription | Should Be 'Moved' - $WebEvent.Response.Headers.Location | Should Be 'https://google.com' + $WebEvent.Response.StatusCode | Should -Be 301 + $WebEvent.Response.StatusDescription | Should -Be 'Moved' + $WebEvent.Response.Headers.Location | Should -Be 'https://google.com' } It 'Alters only the port' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port 9001 - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'http://localhost:9001/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'http://localhost:9001/path' } It 'Alters only the protocol' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Protocol HTTPS - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'https://localhost:8080/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'https://localhost:8080/path' } It 'Alters the port and protocol' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port 9001 -Protocol HTTPS - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'https://localhost:9001/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'https://localhost:9001/path' } It 'Alters the port and protocol as moved' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port 9001 -Protocol HTTPS -Moved - $WebEvent.Response.StatusCode | Should Be 301 - $WebEvent.Response.StatusDescription | Should Be 'Moved' - $WebEvent.Response.Headers.Location | Should Be 'https://localhost:9001/path' + $WebEvent.Response.StatusCode | Should -Be 301 + $WebEvent.Response.StatusDescription | Should -Be 'Moved' + $WebEvent.Response.Headers.Location | Should -Be 'https://localhost:9001/path' } It 'Port is 80 so does not get appended' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port 80 -Protocol Http - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'http://localhost/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'http://localhost/path' } It 'Port is 443 so does not get appended' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port 443 -Protocol HTTPS - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'https://localhost/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'https://localhost/path' } It 'Port is 0 so gets set to URI port' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port 0 -Protocol Http - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'http://localhost:8080/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'http://localhost:8080/path' } It 'Port is negative so gets set to URI port' { $WebEvent = @{ - 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path'} }; + 'Request' = @{ 'Url' = @{ 'Scheme' = 'http'; 'Port' = 8080; 'Host' = 'localhost'; 'PathAndQuery' = '/path' } } 'Response' = @{ 'StatusCode' = 0; 'StatusDescription' = ''; 'Headers' = @{} } } Move-PodeResponseUrl -Port -10 -Protocol Http - $WebEvent.Response.StatusCode | Should Be 302 - $WebEvent.Response.StatusDescription | Should Be 'Redirect' - $WebEvent.Response.Headers.Location | Should Be 'http://localhost:8080/path' + $WebEvent.Response.StatusCode | Should -Be 302 + $WebEvent.Response.StatusDescription | Should -Be 'Redirect' + $WebEvent.Response.Headers.Location | Should -Be 'http://localhost:8080/path' } } } Describe 'Write-PodeJsonResponse' { - Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } - $_ContentType = 'application/json' + BeforeEach { + Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } + $_ContentType = 'application/json' } It 'Returns an empty value for an empty value' { $r = Write-PodeJsonResponse -Value ([string]::Empty) - $r.Value | Should Be '{}' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '{}' + $r.ContentType | Should -Be $_ContentType } It 'Returns a raw value' { $r = Write-PodeJsonResponse -Value '{ "name": "bob" }' - $r.Value | Should Be '{ "name": "bob" }' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '{ "name": "bob" }' + $r.ContentType | Should -Be $_ContentType } It 'Converts and returns a value from a hashtable' { $r = Write-PodeJsonResponse -Value @{ 'name' = 'john' } - $r.Value | Should Be '{"name":"john"}' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '{"name":"john"}' + $r.ContentType | Should -Be $_ContentType } It 'Does nothing for an invalid file path' { Mock Test-PodePath { return $false } Write-PodeJsonResponse -Path 'fake-file' | Out-Null - Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It + Should -Invoke Test-PodePath -Times 1 -Scope It + # Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It } It 'Load the file contents and returns it' { @@ -201,37 +211,39 @@ Describe 'Write-PodeJsonResponse' { Mock Get-PodeFileContent { return '{ "name": "bob" }' } $r = Write-PodeJsonResponse -Path 'file/path' - $r.Value | Should Be '{ "name": "bob" }' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '{ "name": "bob" }' + $r.ContentType | Should -Be $_ContentType } } Describe 'Write-PodeCsvResponse' { - Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } - $_ContentType = 'text/csv' + BeforeEach { + Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } + $_ContentType = 'text/csv' } It 'Returns an empty value for an empty value' { $r = Write-PodeCsvResponse -Value ([string]::Empty) - $r.Value | Should Be ([string]::Empty) - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be ([string]::Empty) + $r.ContentType | Should -Be $_ContentType } It 'Returns a raw value' { $r = Write-PodeCsvResponse -Value 'bob, 42' - $r.Value | Should Be 'bob, 42' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be 'bob, 42' + $r.ContentType | Should -Be $_ContentType } It 'Converts and returns a value from a hashtable' { $r = Write-PodeCsvResponse -Value @{ 'name' = 'john' } - $r.Value | Should Be "`"name`"$([environment]::NewLine)`"john`"" - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be "`"name`"$([environment]::NewLine)`"john`"" + $r.ContentType | Should -Be $_ContentType } It 'Does nothing for an invalid file path' { Mock Test-PodePath { return $false } Write-PodeCsvResponse -Path 'fake-file' | Out-Null - Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It + Should -Invoke Test-PodePath -Times 1 -Scope It + # Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It } It 'Load the file contents and returns it' { @@ -239,37 +251,39 @@ Describe 'Write-PodeCsvResponse' { Mock Get-PodeFileContent { return 'bob, 42' } $r = Write-PodeCsvResponse -Path 'file/path' - $r.Value | Should Be 'bob, 42' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be 'bob, 42' + $r.ContentType | Should -Be $_ContentType } } Describe 'Write-PodeXmlResponse' { - Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } - $_ContentType = 'text/xml' - + BeforeEach { + Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } + $_ContentType = 'text/xml' + } It 'Returns an empty value for an empty value' { $r = Write-PodeXmlResponse -Value ([string]::Empty) - $r.Value | Should Be ([string]::Empty) - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be ([string]::Empty) + $r.ContentType | Should -Be $_ContentType } It 'Returns a raw value' { $r = Write-PodeXmlResponse -Value '' - $r.Value | Should Be '' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '' + $r.ContentType | Should -Be $_ContentType } It 'Converts and returns a value from a hashtable' { $r = Write-PodeXmlResponse -Value @{ 'name' = 'john' } - ($r.Value -ireplace '[\r\n ]', '') | Should Be 'john' - $r.ContentType | Should Be $_ContentType + ($r.Value -ireplace '[\r\n ]', '') | Should -Be 'john' + $r.ContentType | Should -Be $_ContentType } It 'Does nothing for an invalid file path' { Mock Test-PodePath { return $false } Write-PodeXmlResponse -Path 'fake-file' | Out-Null - Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It + Should -Invoke Test-PodePath -Times 1 -Scope It + # Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It } It 'Load the file contents and returns it' { @@ -277,37 +291,39 @@ Describe 'Write-PodeXmlResponse' { Mock Get-PodeFileContent { return '' } $r = Write-PodeXmlResponse -Path 'file/path' - $r.Value | Should Be '' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '' + $r.ContentType | Should -Be $_ContentType } } Describe 'Write-PodeHtmlResponse' { - Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } - $_ContentType = 'text/html' + BeforeEach { + Mock Write-PodeTextResponse { return @{ 'Value' = $Value; 'ContentType' = $ContentType; } } + $_ContentType = 'text/html' } It 'Returns an empty value for an empty value' { $r = Write-PodeHtmlResponse -Value ([string]::Empty) - $r.Value | Should Be ([string]::Empty) - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be ([string]::Empty) + $r.ContentType | Should -Be $_ContentType } It 'Returns a raw value' { $r = Write-PodeHtmlResponse -Value '' - $r.Value | Should Be '' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '' + $r.ContentType | Should -Be $_ContentType } It 'Converts and returns a value from a hashtable' { $r = Write-PodeHtmlResponse -Value @{ 'name' = 'john' } - $r.Value | Should Be ((@{ 'name' = 'john' } | ConvertTo-Html) -join ([environment]::NewLine)) - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be ((@{ 'name' = 'john' } | ConvertTo-Html) -join ([environment]::NewLine)) + $r.ContentType | Should -Be $_ContentType } It 'Does nothing for an invalid file path' { Mock Test-PodePath { return $false } Write-PodeHtmlResponse -Path 'fake-file' | Out-Null - Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It + Should -Invoke Test-PodePath -Times 1 -Scope It + # Assert-MockCalled -CommandName 'Test-PodePath' -Times 1 -Scope It } It 'Load the file contents and returns it' { @@ -315,8 +331,8 @@ Describe 'Write-PodeHtmlResponse' { Mock Get-PodeFileContent { return '' } $r = Write-PodeHtmlResponse -Path 'file/path' - $r.Value | Should Be '' - $r.ContentType | Should Be $_ContentType + $r.Value | Should -Be '' + $r.ContentType | Should -Be $_ContentType } } @@ -333,56 +349,68 @@ Describe 'Write-PodeTextResponse' { Describe 'Write-PodeFileResponse' { It 'Does nothing when the file does not exist' { Mock Get-PodeRelativePath { return $Path } - Mock Test-PodePath { return $false } + # Mock Test-PodePath { return $false } + Mock Set-PodeResponseStatus {} + Mock Get-Item { return $null } Write-PodeFileResponse -Path './path' | Out-Null - Assert-MockCalled Test-PodePath -Times 1 -Scope It + Should -Invoke Set-PodeResponseStatus -Times 1 -Scope It + # Assert-MockCalled Test-PodePath -Times 1 -Scope It } - Mock Test-PodePath { return $true } It 'Loads the contents of a dynamic file' { + Mock Test-PodePath { return $true } Mock Get-PodeRelativePath { return $Path } Mock Get-PodeFileContentUsingViewEngine { return 'file contents' } Mock Write-PodeTextResponse { return $Value } + Mock Get-Item { return @{ PSIsContainer = $false } } - Write-PodeFileResponse -Path './path/file.pode' | Should Be 'file contents' - - Assert-MockCalled Get-PodeFileContentUsingViewEngine -Times 1 -Scope It + Write-PodeFileResponse -Path './path/file.pode' | Should -Be 'file contents' + Should -Invoke Get-PodeFileContentUsingViewEngine -Times 1 -Scope It + #Assert-MockCalled Get-PodeFileContentUsingViewEngine -Times 1 -Scope It } It 'Loads the contents of a static file' { + + Mock Test-PodePath { return $true } Mock Get-PodeRelativePath { return $Path } Mock Get-Content { return 'file contents' } + Mock Get-PodeFileContentUsingViewEngine { return 'file contents' } Mock Write-PodeTextResponse { return $Value } - - Write-PodeFileResponse -Path './path/file.pode' | Should Be 'file contents' - - Assert-MockCalled Get-PodeFileContentUsingViewEngine -Times 1 -Scope It + Mock Get-Item { return @{ PSIsContainer = $false } } + Write-PodeFileResponse -Path './path/file.pode' | Should -Be 'file contents' + Should -Invoke Get-PodeFileContentUsingViewEngine -Times 1 -Scope It + # Assert-MockCalled Get-PodeFileContentUsingViewEngine -Times 1 -Scope It } } Describe 'Use-PodePartialView' { - $PodeContext = @{ - 'Server' = @{ - 'InbuiltDrives' = @{ 'views' = '.' } - 'ViewEngine' = @{ 'Extension' = 'pode' } + BeforeEach { + $PodeContext = @{ + 'Server' = @{ + 'InbuiltDrives' = @{ 'views' = '.' } + 'ViewEngine' = @{ 'Extension' = 'pode' } + } } + Mock Get-PodeFileContentUsingViewEngine { return 'file contents' } } It 'Throws an error for a path that does not exist' { Mock Test-PodePath { return $false } - { Use-PodePartialView -Path 'sub-view.pode' } | Should Throw 'File not found' + { Use-PodePartialView -Path 'sub-view.pode' } | Should -Throw -ExpectedMessage '*File not found*' } - Mock Test-PodePath { return $true } - Mock Get-PodeFileContentUsingViewEngine { return 'file contents' } + + It 'Returns file contents, and appends view engine' { - Use-PodePartialView -Path 'sub-view' | Should Be 'file contents' + Mock Test-PodePath { return $true } + Use-PodePartialView -Path 'sub-view' | Should -Be 'file contents' } It 'Returns file contents' { - Use-PodePartialView -Path 'sub-view.pode' | Should Be 'file contents' + Mock Test-PodePath { return $true } + Use-PodePartialView -Path 'sub-view.pode' | Should -Be 'file contents' } } @@ -395,31 +423,38 @@ Describe 'Close-PodeTcpClient' { } Describe 'Show-PodeErrorPage' { - Mock Write-PodeFileResponse { return $Data } - + BeforeEach { + Mock Write-PodeFileResponse { return $Data } + } It 'Does nothing when it cannot find a page' { Mock Find-PodeErrorPage { return $null } Show-PodeErrorPage -Code 404 | Out-Null - Assert-MockCalled Write-PodeFileResponse -Times 0 -Scope It + Should -Invoke Write-PodeFileResponse -Times 0 -Scope It + # Assert-MockCalled Write-PodeFileResponse -Times 0 -Scope It } - Mock Find-PodeErrorPage { return @{ 'Path' = './path'; 'ContentType' = 'json' } } - Mock Get-PodeUrl { return 'url' } It 'Renders a page with no exception' { + Mock Find-PodeErrorPage { return @{ 'Path' = './path'; 'ContentType' = 'json' } } + Mock Get-PodeUrl { return 'url' } $d = Show-PodeErrorPage -Code 404 - - Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It - $d.Url | Should Be 'url' - $d.Exception | Should Be $null - $d.ContentType | Should Be 'json' - $d.Status.Code | Should Be 404 + Should -Invoke Write-PodeFileResponse -Times 1 -Scope It + #Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It + $d.Url | Should -Be 'url' + $d.Exception | Should -Be $null + $d.ContentType | Should -Be 'json' + $d.Status.Code | Should -Be 404 } It 'Renders a page with exception' { + + Mock Find-PodeErrorPage { return @{ 'Path' = './path'; 'ContentType' = 'json' } } + Mock Get-PodeUrl { return 'url' } $PodeContext = @{ 'Server' = @{ 'Web' = @{ - 'ErrorPages' = @{ 'ShowExceptions' = $true } - } } } + 'ErrorPages' = @{ 'ShowExceptions' = $true } + } + } + } try { $v = $null @@ -428,15 +463,15 @@ Describe 'Show-PodeErrorPage' { catch { $e = $_ } $d = Show-PodeErrorPage -Code 404 -Exception $e - - Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It - $d.Url | Should Be 'url' - $d.Exception | Should Not Be $null - $d.Exception.Message | Should Match 'cannot call a method' - $d.Exception.Category | Should Match 'InvalidOperation' - $d.Exception.StackTrace | Should Match 'Responses.Tests.ps1' - $d.Exception.Line | Should Match 'Responses.Tests.ps1' - $d.ContentType | Should Be 'json' - $d.Status.Code | Should Be 404 + Should -Invoke Write-PodeFileResponse -Times 1 -Scope It + #Assert-MockCalled Write-PodeFileResponse -Times 1 -Scope It + $d.Url | Should -Be 'url' + $d.Exception | Should -Not -Be $null + $d.Exception.Message | Should -Match 'cannot call a method' + $d.Exception.Category | Should -Match 'InvalidOperation' + $d.Exception.StackTrace | Should -Match 'Responses.Tests.ps1' + $d.Exception.Line | Should -Match 'Responses.Tests.ps1' + $d.ContentType | Should -Be 'json' + $d.Status.Code | Should -Be 404 } } \ No newline at end of file diff --git a/tests/unit/Routes.Tests.ps1 b/tests/unit/Routes.Tests.ps1 index 5ae80a3d2..c3dbebf65 100644 --- a/tests/unit/Routes.Tests.ps1 +++ b/tests/unit/Routes.Tests.ps1 @@ -1,79 +1,86 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } - -$PodeContext = @{ 'Server' = $null; } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + $PodeContext = @{ 'Server' = $null; } +} Describe 'Find-PodeRoute' { Context 'Invalid parameters supplied' { It 'Throw invalid method error for no method' { - { Find-PodeRoute -Method 'MOO' -Path '/' } | Should Throw "Cannot validate argument on parameter 'Method'" + { Find-PodeRoute -Method 'MOO' -Path '/' } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'Method'*" } It 'Throw null route parameter error' { - { Find-PodeRoute -Method GET -Path $null } | Should Throw 'The argument is null or empty' + { Find-PodeRoute -Method GET -Path $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } It 'Throw empty route parameter error' { - { Find-PodeRoute -Method GET -Path ([string]::Empty) } | Should Throw 'The argument is null or empty' + { Find-PodeRoute -Method GET -Path ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } } Context 'Valid method and route' { It 'Return null as method does not exist' { $PodeContext.Server = @{ 'Routes' = @{}; } - Find-PodeRoute -Method GET -Path '/' | Should Be $null + Find-PodeRoute -Method GET -Path '/' | Should -Be $null } It 'Returns no logic for method/route that do not exist' { $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; } - Find-PodeRoute -Method GET -Path '/' | Should Be $null + Find-PodeRoute -Method GET -Path '/' | Should -Be $null } It 'Returns logic for method and exact route' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Logic'= { Write-Host 'Test' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Root' = '/'; 'Logic' = { Write-Host 'Test' }; }); }; }; } $result = (Find-PodeRoute -Method GET -Path '/') - $result | Should BeOfType System.Collections.Hashtable - $result.Logic.ToString() | Should Be ({ Write-Host 'Test' }).ToString() + $result | Should -BeOfType System.Collections.Hashtable + $result.Logic.ToString() | Should -Be ({ Write-Host 'Test' }).ToString() } It 'Returns logic for method and exact route and endpoint' { $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @( - @{ 'Logic'= { Write-Host 'Test' }; }; - @{ 'Logic'= { Write-Host 'Test' }; 'Endpoint' = @{ Name = 'example'; 'Address' = 'pode.foo.com' } }; - ); }; }; } + @{ 'Root' = '/'; 'Logic' = { Write-Host 'Test' }; } + @{ 'Root' = '/'; 'Logic' = { Write-Host 'Test' }; 'Endpoint' = @{ Name = 'example'; 'Address' = 'pode.foo.com' } } + ) + } + } + } $result = (Find-PodeRoute -Method GET -Path '/' -EndpointName 'example') - $result | Should BeOfType System.Collections.Hashtable - $result.Endpoint.Address | Should Be 'pode.foo.com' - $result.Logic.ToString() | Should Be ({ Write-Host 'Test' }).ToString() + $result | Should -BeOfType System.Collections.Hashtable + $result.Endpoint.Address | Should -Be 'pode.foo.com' + $result.Logic.ToString() | Should -Be ({ Write-Host 'Test' }).ToString() } It 'Returns logic and middleware for method and exact route' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{ 'Logic'= { Write-Host 'Test' }; 'Middleware' = { Write-Host 'Middle' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @(@{'Root'='/'; 'Logic' = { Write-Host 'Test' }; 'Middleware' = { Write-Host 'Middle' }; }); }; }; } $result = (Find-PodeRoute -Method GET -Path '/') - $result | Should BeOfType System.Collections.Hashtable - $result.Logic.ToString() | Should Be ({ Write-Host 'Test' }).ToString() - $result.Middleware.ToString() | Should Be ({ Write-Host 'Middle' }).ToString() + $result | Should -BeOfType System.Collections.Hashtable + $result.Logic.ToString() | Should -Be ({ Write-Host 'Test' }).ToString() + $result.Middleware.ToString() | Should -Be ({ Write-Host 'Middle' }).ToString() } It 'Returns logic for method and exact route under star' { - $PodeContext.Server = @{ 'Routes' = @{ '*' = @{ '/' = @(@{ 'Logic'= { Write-Host 'Test' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ '*' = @{ '/' = @(@{ 'Root'='/'; 'Logic' = { Write-Host 'Test' }; }); }; }; } $result = (Find-PodeRoute -Method * -Path '/') - $result | Should BeOfType System.Collections.Hashtable - $result.Logic.ToString() | Should Be ({ Write-Host 'Test' }).ToString() + $result | Should -BeOfType System.Collections.Hashtable + $result.Logic.ToString() | Should -Be ({ Write-Host 'Test' }).ToString() } It 'Returns logic and parameters for parameterised route' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/(?[^\/]+?)' = @(@{ 'Logic'= { Write-Host 'Test' }; }); }; }; } + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/(?[^\/]+?)' = @(@{ 'Root'='/'; 'Logic' = { Write-Host 'Test' }; }); }; }; } $result = (Find-PodeRoute -Method GET -Path '/123') - $result | Should BeOfType System.Collections.Hashtable - $result.Logic.ToString() | Should Be ({ Write-Host 'Test' }).ToString() + $result | Should -BeOfType System.Collections.Hashtable + $result.Logic.ToString() | Should -Be ({ Write-Host 'Test' }).ToString() } } } @@ -87,38 +94,45 @@ Describe 'Add-PodeStaticRoute' { Add-PodeStaticRoute -Path '/assets' -Source './assets' $route = $PodeContext.Server.Routes['static'] - $route | Should Not Be $null - $route.ContainsKey('/assets[/]{0,1}(?.*)') | Should Be $true - $route['/assets[/]{0,1}(?.*)'].Source | Should Be './assets' + $route | Should -Not -Be $null + $route.ContainsKey('/assets[/]{0,1}(?.*)') | Should -Be $true + $route['/assets[/]{0,1}(?.*)'].Source | Should -Be './assets' } It 'Throws error when adding static route for non-existing folder' { Mock Test-PodePath { return $false } $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd; FindEndpoints = @{} } - { Add-PodeStaticRoute -Path '/assets' -Source './assets' } | Should Throw 'does not exist' + { Add-PodeStaticRoute -Path '/assets' -Source './assets' } | Should -Throw -ExpectedMessage '*does not exist*' } } Describe 'Remove-PodeRoute' { + BeforeEach { + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{}; 'Endpoints' = @{}; 'EndpointsMap' = @{} + 'OpenAPI' = @{ + SelectedDefinitionTag = 'default' + Definitions = @{ + default = Get-PodeOABaseObject + } + } + } + } It 'Adds route with simple url, and then removes it' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } - Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'].Length | Should Be 1 + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'].Length | Should -Be 1 Remove-PodeRoute -Method Get -Path '/users' $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $false + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $false } It 'Adds two routes with simple url, and then removes one' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user @@ -126,16 +140,16 @@ Describe 'Remove-PodeRoute' { Add-PodeRoute -Method Get -Path '/users' -EndpointName user -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'].Length | Should Be 2 + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'].Length | Should -Be 2 Remove-PodeRoute -Method Get -Path '/users' $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'].Length | Should Be 1 + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'].Length | Should -Be 1 } } @@ -148,57 +162,65 @@ Describe 'Remove-PodeStaticRoute' { Add-PodeStaticRoute -Path '/assets' -Source './assets' $routes = $PodeContext.Server.Routes['static'] - $routes | Should Not be $null - $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should Be $true - $routes['/assets[/]{0,1}(?.*)'].Source | Should Be './assets' + $routes | Should -Not -Be $null + $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should -Be $true + $routes['/assets[/]{0,1}(?.*)'].Source | Should -Be './assets' Remove-PodeStaticRoute -Path '/assets' $routes = $PodeContext.Server.Routes['static'] - $routes | Should Not be $null - $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should Be $false + $routes | Should -Not -Be $null + $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should -Be $false } } Describe 'Clear-PodeRoutes' { + BeforeEach { + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{} } + 'FindEndpoints' = @{} + 'OpenAPI' = @{ + SelectedDefinitionTag = 'default' + Definitions = @{ + default = Get-PodeOABaseObject + } + } + } } It 'Adds routes for methods, and clears everything' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello1' } Add-PodeRoute -Method POST -Path '/messages' -ScriptBlock { Write-Host 'hello2' } $routes = $PodeContext.Server.Routes['get'] - $routes.ContainsKey('/users') | Should Be $true + $routes.ContainsKey('/users') | Should -Be $true $routes = $PodeContext.Server.Routes['post'] - $routes.ContainsKey('/messages') | Should Be $true + $routes.ContainsKey('/messages') | Should -Be $true Clear-PodeRoutes $routes = $PodeContext.Server.Routes['get'] - $routes.ContainsKey('/users') | Should Be $false + $routes.ContainsKey('/users') | Should -Be $false $routes = $PodeContext.Server.Routes['post'] - $routes.ContainsKey('/messages') | Should Be $false + $routes.ContainsKey('/messages') | Should -Be $false } It 'Adds routes for methods, and clears one method' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello1' } Add-PodeRoute -Method POST -Path '/messages' -ScriptBlock { Write-Host 'hello2' } $routes = $PodeContext.Server.Routes['get'] - $routes.ContainsKey('/users') | Should Be $true + $routes.ContainsKey('/users') | Should -Be $true $routes = $PodeContext.Server.Routes['post'] - $routes.ContainsKey('/messages') | Should Be $true + $routes.ContainsKey('/messages') | Should -Be $true Clear-PodeRoutes -Method Get $routes = $PodeContext.Server.Routes['get'] - $routes.ContainsKey('/users') | Should Be $false + $routes.ContainsKey('/users') | Should -Be $false $routes = $PodeContext.Server.Routes['post'] - $routes.ContainsKey('/messages') | Should Be $true + $routes.ContainsKey('/messages') | Should -Be $true } } @@ -213,81 +235,90 @@ Describe 'Clear-PodeStaticRoutes' { Add-PodeStaticRoute -Path '/images' -Source './images' $routes = $PodeContext.Server.Routes['static'] - $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should Be $true - $routes.ContainsKey('/images[/]{0,1}(?.*)') | Should Be $true + $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should -Be $true + $routes.ContainsKey('/images[/]{0,1}(?.*)') | Should -Be $true Clear-PodeStaticRoutes $routes = $PodeContext.Server.Routes['static'] - $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should Be $false - $routes.ContainsKey('/images[/]{0,1}(?.*)') | Should Be $false + $routes.ContainsKey('/assets[/]{0,1}(?.*)') | Should -Be $false + $routes.ContainsKey('/images[/]{0,1}(?.*)') | Should -Be $false } } Describe 'Add-PodeRoute' { + BeforeEach { + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} + 'Endpoints' = @{} + 'OpenAPI' = @{ + SelectedDefinitionTag = 'default' + Definitions = @{ + default = Get-PodeOABaseObject + } + } + } + } It 'Throws invalid method error for no method' { - { Add-PodeRoute -Method 'MOO' -Path '/' -ScriptBlock {} } | Should Throw "Cannot validate argument on parameter 'Method'" + { Add-PodeRoute -Method 'MOO' -Path '/' -ScriptBlock {} } | Should -Throw -ExpectedMessage "*Cannot validate argument on parameter 'Method'*" } It 'Throws null route parameter error' { - { Add-PodeRoute -Method GET -Path $null -ScriptBlock {} } | Should Throw 'it is an empty string' + { Add-PodeRoute -Method GET -Path $null -ScriptBlock {} } | Should -Throw -ExpectedMessage '*it is an empty string*' } It 'Throws empty route parameter error' { - { Add-PodeRoute -Method GET -Path ([string]::Empty) -ScriptBlock {} } | Should Throw 'it is an empty string' + { Add-PodeRoute -Method GET -Path ([string]::Empty) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*it is an empty string*' } It 'Throws error when scriptblock and file path supplied' { - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } -FilePath './path' } | Should Throw 'parameter set cannot be resolved' + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } -FilePath './path' } | Should -Throw -ExpectedMessage '*parameter set cannot be resolved*' } It 'Throws error when file path is a directory' { Mock Get-PodeRelativePath { return $Path } Mock Test-PodePath { return $true } - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{} }; 'FindEndpoints' = @{} } - { Add-PodeRoute -Method GET -Path '/' -FilePath './path' } | Should Throw 'cannot be a wildcard or a directory' + { Add-PodeRoute -Method GET -Path '/' -FilePath './path' } | Should -Throw -ExpectedMessage '*cannot be a wildcard or a directory*' } It 'Throws error when file path is a wildcard' { Mock Get-PodeRelativePath { return $Path } Mock Test-PodePath { return $true } - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{} }; 'FindEndpoints' = @{} } - { Add-PodeRoute -Method GET -Path '/' -FilePath './path/*' } | Should Throw 'cannot be a wildcard or a directory' + { Add-PodeRoute -Method GET -Path '/' -FilePath './path/*' } | Should -Throw -ExpectedMessage '*cannot be a wildcard or a directory*' } It 'Throws error because no scriptblock supplied' { - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should Throw "No logic passed" + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*No logic passed*' } It 'Throws error because only querystring has been given' { - { Add-PodeRoute -Method GET -Path "?k=v" -ScriptBlock { write-host 'hi' } } | Should Throw "No path supplied" + { Add-PodeRoute -Method GET -Path '?k=v' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage '*No path supplied*' } It 'Throws error because route already exists' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{ '/' = @( - @{ 'Endpoint' = @{'Protocol' = ''; 'Address' = ''} } - ); }; }; 'FindEndpoints' = @{} } + $PodeContext.Server['Routes'] = @{ 'GET' = @{ '/' = @( + @{ 'Endpoint' = @{'Protocol' = ''; 'Address' = '' } } + ) + } + } - { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } } | Should Throw 'already defined' + { Add-PodeRoute -Method GET -Path '/' -ScriptBlock { write-host 'hi' } } | Should -Throw -ExpectedMessage '*already defined*' } It 'Throws error on GET route for endpoint name not existing' { - $PodeContext.Server = @{ 'Endpoints' = @{}; 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } - { Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName 'test' } | Should Throw 'does not exist' + { Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName 'test' } | Should -Throw -ExpectedMessage '*does not exist*' } It 'Adds route with simple url' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'] | Should Not Be $null - $routes['/users'].Length | Should Be 1 - $routes['/users'][0].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $routes['/users'][0].Middleware | Should Be $null - $routes['/users'][0].ContentType | Should Be ([string]::Empty) + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'] | Should -Not -Be $null + $routes['/users'].Length | Should -Be 1 + $routes['/users'][0].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $routes['/users'][0].Middleware | Should -Be $null + $routes['/users'][0].ContentType | Should -Be ([string]::Empty) } It 'Adds route with simple url and scriptblock from file path' { @@ -295,284 +326,265 @@ Describe 'Add-PodeRoute' { Mock Test-PodePath { return $true } Mock Use-PodeScript { return { Write-Host 'bye' } } - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -FilePath './path/route.ps1' $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'] | Should Not Be $null - $routes['/users'].Length | Should Be 1 - $routes['/users'][0].Logic.ToString() | Should Be ({ Write-Host 'bye' }).ToString() - $routes['/users'][0].Middleware | Should Be $null - $routes['/users'][0].ContentType | Should Be ([string]::Empty) + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'] | Should -Not -Be $null + $routes['/users'].Length | Should -Be 1 + $routes['/users'][0].Logic.ToString() | Should -Be ({ Write-Host 'bye' }).ToString() + $routes['/users'][0].Middleware | Should -Be $null + $routes['/users'][0].ContentType | Should -Be ([string]::Empty) } Mock Test-PodePath { return $false } It 'Adds route with simple url with content type' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -ContentType 'application/json' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'] | Should Not Be $null - $routes['/users'].Length | Should Be 1 - $routes['/users'][0].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $routes['/users'][0].Middleware | Should Be $null - $routes['/users'][0].ContentType | Should Be 'application/json' + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'] | Should -Not -Be $null + $routes['/users'].Length | Should -Be 1 + $routes['/users'][0].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $routes['/users'][0].Middleware | Should -Be $null + $routes['/users'][0].ContentType | Should -Be 'application/json' } It 'Adds route with simple url with default content type' { - $PodeContext.Server = @{ - 'Routes' = @{ 'GET' = @{}; }; - 'Web' = @{ 'ContentType' = @{ - 'Default' = 'text/xml'; - 'Routes' = @{}; - } }; - 'FindEndpoints' = @{} + $PodeContext.Server['Web'] = @{ 'ContentType' = @{ + 'Default' = 'text/xml' + 'Routes' = @{} + } } + Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'] | Should Not Be $null - $routes['/users'].Length | Should Be 1 - $routes['/users'][0].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $routes['/users'][0].Middleware | Should Be $null - $routes['/users'][0].ContentType | Should Be 'text/xml' + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'] | Should -Not -Be $null + $routes['/users'].Length | Should -Be 1 + $routes['/users'][0].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $routes['/users'][0].Middleware | Should -Be $null + $routes['/users'][0].ContentType | Should -Be 'text/xml' } It 'Adds route with simple url with route pattern content type' { - $PodeContext.Server = @{ - 'Routes' = @{ 'GET' = @{}; }; - 'Web' = @{ 'ContentType' = @{ - 'Default' = 'text/xml'; - 'Routes' = @{ '/users' = 'text/plain' }; - } }; - 'FindEndpoints' = @{} + $PodeContext.Server['Web'] = @{ 'ContentType' = @{ + 'Default' = 'text/xml' + 'Routes' = @{ '/users' = 'text/plain' } + } } Add-PodeRoute -Method GET -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null - $routes.ContainsKey('/users') | Should Be $true - $routes['/users'] | Should Not Be $null - $routes['/users'].Length | Should Be 1 - $routes['/users'][0].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $routes['/users'][0].Middleware | Should Be $null - $routes['/users'][0].ContentType | Should Be 'text/plain' + $routes | Should -Not -Be $null + $routes.ContainsKey('/users') | Should -Be $true + $routes['/users'] | Should -Not -Be $null + $routes['/users'].Length | Should -Be 1 + $routes['/users'][0].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $routes['/users'][0].Middleware | Should -Be $null + $routes['/users'][0].ContentType | Should -Be 'text/plain' } It 'Adds route with middleware supplied as scriptblock and no logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -Middleware ({ Write-Host 'middle' }) -ScriptBlock {} $route = $PodeContext.Server.Routes['get'] - $route | Should Not be $null + $route | Should -Not -Be $null $route = $route['/users'] - $route | Should Not Be $null + $route | Should -Not -Be $null - $route.Middleware.Logic.ToString() | Should Be ({ Write-Host 'middle' }).ToString() - $route.Logic | Should Be ({}).ToString() + $route.Middleware.Logic.ToString() | Should -Be ({ Write-Host 'middle' }).ToString() + $route.Logic | Should -Be ({}).ToString() } It 'Adds route with middleware supplied as hashtable with null logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } - { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = $null }) -ScriptBlock {} } | Should Throw 'no logic defined' + { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = $null }) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*no logic defined*' } It 'Adds route with middleware supplied as hashtable with invalid type logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } - { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = 74 }) -ScriptBlock {} } | Should Throw 'invalid logic type' + { Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = 74 }) -ScriptBlock {} } | Should -Throw -ExpectedMessage '*invalid logic type*' } It 'Adds route with invalid middleware type' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } - { Add-PodeRoute -Method GET -Path '/users' -Middleware 74 -ScriptBlock {} } | Should Throw 'invalid type' + { Add-PodeRoute -Method GET -Path '/users' -Middleware 74 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*invalid type*' } It 'Adds route with middleware supplied as hashtable and empty logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{}} Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = { Write-Host 'middle' }; 'Arguments' = 'test' }) -ScriptBlock {} $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null + $routes | Should -Not -Be $null $routes = $routes['/users'] - $routes | Should Not Be $null - $routes.Length | Should Be 1 + $routes | Should -Not -Be $null + $routes.Length | Should -Be 1 - $routes[0].Logic.ToString() | Should Be ({}).ToString() - $routes[0].Endpoint.Protocol | Should Be '' - $routes[0].Endpoint.Address | Should Be '' + $routes[0].Logic.ToString() | Should -Be ({}).ToString() + $routes[0].Endpoint.Protocol | Should -Be '' + $routes[0].Endpoint.Address | Should -Be '' - $routes[0].Middleware.Length | Should Be 1 - $routes[0].Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle' }).ToString() - $routes[0].Middleware[0].Arguments | Should Be 'test' + $routes[0].Middleware.Length | Should -Be 1 + $routes[0].Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle' }).ToString() + $routes[0].Middleware[0].Arguments | Should -Be 'test' } It 'Adds route with middleware supplied as hashtable and no logic' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -Middleware (@{ 'Logic' = { Write-Host 'middle' }; 'Arguments' = 'test' }) -ScriptBlock {} $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null + $routes | Should -Not -Be $null $routes = $routes['/users'] - $routes | Should Not Be $null - $routes.Length | Should Be 1 + $routes | Should -Not -Be $null + $routes.Length | Should -Be 1 - $routes[0].Logic.ToString() | Should Be ({}).ToString() - $routes[0].Endpoint.Protocol | Should Be '' - $routes[0].Endpoint.Address | Should Be '' + $routes[0].Logic.ToString() | Should -Be ({}).ToString() + $routes[0].Endpoint.Protocol | Should -Be '' + $routes[0].Endpoint.Address | Should -Be '' - $routes[0].Middleware.Length | Should Be 1 - $routes[0].Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle' }).ToString() - $routes[0].Middleware[0].Arguments | Should Be 'test' + $routes[0].Middleware.Length | Should -Be 1 + $routes[0].Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle' }).ToString() + $routes[0].Middleware[0].Arguments | Should -Be 'test' } It 'Adds route with middleware and logic supplied' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -Middleware { Write-Host 'middle' } -ScriptBlock { Write-Host 'logic' } $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null + $routes | Should -Not -Be $null $routes = $routes['/users'] - $routes | Should Not Be $null - $routes.Length | Should Be 1 + $routes | Should -Not -Be $null + $routes.Length | Should -Be 1 - $routes[0].Logic.ToString() | Should Be ({ Write-Host 'logic' }).ToString() - $routes[0].Endpoint.Protocol | Should Be '' - $routes[0].Endpoint.Address | Should Be '' + $routes[0].Logic.ToString() | Should -Be ({ Write-Host 'logic' }).ToString() + $routes[0].Endpoint.Protocol | Should -Be '' + $routes[0].Endpoint.Address | Should -Be '' - $routes[0].Middleware.Length | Should Be 1 - $routes[0].Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle' }).ToString() + $routes[0].Middleware.Length | Should -Be 1 + $routes[0].Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle' }).ToString() } It 'Adds route with array of middleware and no logic supplied' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } - Add-PodeRoute -Method GET -Path '/users' -Middleware @( { Write-Host 'middle1' }, { Write-Host 'middle2' } - ) -ScriptBlock {} + ) -ScriptBlock {} $routes = $PodeContext.Server.Routes['get'] - $routes | Should Not be $null + $routes | Should -Not -Be $null $routes = $routes['/users'] - $routes | Should Not Be $null - $routes.Length | Should Be 1 + $routes | Should -Not -Be $null + $routes.Length | Should -Be 1 - $routes[0].Logic.ToString() | Should Be ({}).ToString() + $routes[0].Logic.ToString() | Should -Be ({}).ToString() - $routes[0].Middleware.Length | Should Be 2 - $routes[0].Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $routes[0].Middleware[1].Logic.ToString() | Should Be ({ Write-Host 'middle2' }).ToString() + $routes[0].Middleware.Length | Should -Be 2 + $routes[0].Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $routes[0].Middleware[1].Logic.ToString() | Should -Be ({ Write-Host 'middle2' }).ToString() } It 'Adds route with array of middleware and logic supplied' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users' -Middleware @( { Write-Host 'middle1' }, { Write-Host 'middle2' } - ) -ScriptBlock { Write-Host 'logic' } + ) -ScriptBlock { Write-Host 'logic' } $route = $PodeContext.Server.Routes['get'] - $route | Should Not be $null + $route | Should -Not -Be $null $route = $route['/users'] - $route | Should Not Be $null + $route | Should -Not -Be $null - $route.Logic.ToString() | Should Be ({ Write-Host 'logic' }).ToString() - $route.Middleware.Length | Should Be 2 - $route.Middleware[0].Logic.ToString() | Should Be ({ Write-Host 'middle1' }).ToString() - $route.Middleware[1].Logic.ToString() | Should Be ({ Write-Host 'middle2' }).ToString() + $route.Logic.ToString() | Should -Be ({ Write-Host 'logic' }).ToString() + $route.Middleware.Length | Should -Be 2 + $route.Middleware[0].Logic.ToString() | Should -Be ({ Write-Host 'middle1' }).ToString() + $route.Middleware[1].Logic.ToString() | Should -Be ({ Write-Host 'middle2' }).ToString() } It 'Adds route with simple url and querystring' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users?k=v' -ScriptBlock { Write-Host 'hello' } $route = $PodeContext.Server.Routes['get'] - $route | Should Not be $null - $route.ContainsKey('/users') | Should Be $true - $route['/users'] | Should Not Be $null - $route['/users'].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $route['/users'].Middleware | Should Be $null + $route | Should -Not -Be $null + $route.ContainsKey('/users') | Should -Be $true + $route['/users'] | Should -Not -Be $null + $route['/users'].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $route['/users'].Middleware | Should -Be $null } It 'Adds route with url parameters' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users/:userId' -ScriptBlock { Write-Host 'hello' } $route = $PodeContext.Server.Routes['get'] - $route | Should Not be $null - $route.ContainsKey('/users/(?[^\/]+?)') | Should Be $true - $route['/users/(?[^\/]+?)'] | Should Not Be $null - $route['/users/(?[^\/]+?)'].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $route['/users/(?[^\/]+?)'].Middleware | Should Be $null + $route | Should -Not -Be $null + $route.ContainsKey('/users/(?[^\/]+?)') | Should -Be $true + $route['/users/(?[^\/]+?)'] | Should -Not -Be $null + $route['/users/(?[^\/]+?)'].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $route['/users/(?[^\/]+?)'].Middleware | Should -Be $null } It 'Adds route with url parameters and querystring' { - $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; }; 'FindEndpoints' = @{} } Add-PodeRoute -Method GET -Path '/users/:userId?k=v' -ScriptBlock { Write-Host 'hello' } $route = $PodeContext.Server.Routes['get'] - $route | Should Not be $null - $route.ContainsKey('/users/(?[^\/]+?)') | Should Be $true - $route['/users/(?[^\/]+?)'] | Should Not Be $null - $route['/users/(?[^\/]+?)'].Logic.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $route['/users/(?[^\/]+?)'].Middleware | Should Be $null + $route | Should -Not -Be $null + $route.ContainsKey('/users/(?[^\/]+?)') | Should -Be $true + $route['/users/(?[^\/]+?)'] | Should -Not -Be $null + $route['/users/(?[^\/]+?)'].Logic.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $route['/users/(?[^\/]+?)'].Middleware | Should -Be $null } } Describe 'Convert-PodeFunctionVerbToHttpMethod' { It 'Returns POST for no Verb' { - Convert-PodeFunctionVerbToHttpMethod -Verb ([string]::Empty) | Should Be 'POST' + Convert-PodeFunctionVerbToHttpMethod -Verb ([string]::Empty) | Should -Be 'POST' } It 'Returns POST' { - Convert-PodeFunctionVerbToHttpMethod -Verb Invoke | Should Be 'POST' + Convert-PodeFunctionVerbToHttpMethod -Verb Invoke | Should -Be 'POST' } It 'Returns GET' { - Convert-PodeFunctionVerbToHttpMethod -Verb Find | Should Be 'GET' + Convert-PodeFunctionVerbToHttpMethod -Verb Find | Should -Be 'GET' } It 'Returns PUT' { - Convert-PodeFunctionVerbToHttpMethod -Verb Set | Should Be 'PUT' + Convert-PodeFunctionVerbToHttpMethod -Verb Set | Should -Be 'PUT' } It 'Returns PATCH' { - Convert-PodeFunctionVerbToHttpMethod -Verb Edit | Should Be 'PATCH' + Convert-PodeFunctionVerbToHttpMethod -Verb Edit | Should -Be 'PATCH' } It 'Returns DELETE' { - Convert-PodeFunctionVerbToHttpMethod -Verb Remove | Should Be 'DELETE' + Convert-PodeFunctionVerbToHttpMethod -Verb Remove | Should -Be 'DELETE' } } Describe 'ConvertTo-PodeRoute' { - Mock Import-PodeModule {} - Mock Write-Verbose {} - Mock Add-PodeRoute {} - Mock Write-PodeJsonResponse {} - Mock Get-Module { return @{ ExportedCommands = @{ Keys = @('Some-ModuleCommand1', 'Some-ModuleCommand2') } } } - + BeforeAll { + Mock Import-PodeModule {} + Mock Write-Verbose {} + Mock Add-PodeRoute {} + Mock Write-PodeJsonResponse {} + Mock Get-Module { return @{ ExportedCommands = @{ Keys = @('Some-ModuleCommand1', 'Some-ModuleCommand2') } } } + } It 'Throws error when module does not contain command' { - { ConvertTo-PodeRoute -Module Example -Commands 'Get-ChildItem' } | Should Throw 'does not contain function' + { ConvertTo-PodeRoute -Module Example -Commands 'Get-ChildItem' } | Should -Throw -ExpectedMessage '*does not contain function*' } It 'Throws error for no commands' { - { ConvertTo-PodeRoute } | Should Throw 'No commands supplied to convert to Routes' + { ConvertTo-PodeRoute } | Should -Throw -ExpectedMessage 'No commands supplied to convert to Routes' } It 'Calls Add-PodeRoute twice for commands' { @@ -592,19 +604,21 @@ Describe 'ConvertTo-PodeRoute' { } Describe 'Add-PodePage' { - Mock Add-PodeRoute {} + BeforeAll { + Mock Add-PodeRoute {} + } It 'Throws error for invalid Name' { - { Add-PodePage -Name 'Rick+Morty' -ScriptBlock {} } | Should Throw 'should be a valid alphanumeric' + { Add-PodePage -Name 'Rick+Morty' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*should be a valid alphanumeric*' } It 'Throws error for invalid ScriptBlock' { - { Add-PodePage -Name 'RickMorty' -ScriptBlock {} } | Should Throw 'non-empty scriptblock is required' + { Add-PodePage -Name 'RickMorty' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*non-empty scriptblock is required*' } It 'Throws error for invalid FilePath' { $PodeContext.Server = @{ 'Root' = $pwd } - { Add-PodePage -Name 'RickMorty' -FilePath './fake/path' } | Should Throw 'the path does not exist' + { Add-PodePage -Name 'RickMorty' -FilePath './fake/path' } | Should -Throw -ExpectedMessage '*the path does not exist*' } It 'Call Add-PodeRoute once for ScriptBlock page' { @@ -627,167 +641,168 @@ Describe 'Add-PodePage' { Describe 'Update-PodeRouteSlashes' { Context 'Static' { It 'Update route slashes' { - $input = '/route' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route[/]{0,1}(?.*)' + $in = '/route' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, no slash' { - $input = 'route' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route[/]{0,1}(?.*)' + $in = 'route' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, ending with wildcard' { - $input = '/route/*' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route[/]{0,1}(?.*)' + $in = '/route/*' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, ending with wildcard, no slash' { - $input = 'route/*' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route[/]{0,1}(?.*)' + $in = 'route/*' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard' { - $input = '/route/*/ending' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route/.*/ending[/]{0,1}(?.*)' + $in = '/route/*/ending' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard, no slash' { - $input = 'route/*/ending' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route/.*/ending[/]{0,1}(?.*)' + $in = 'route/*/ending' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard, ending with wildcard' { - $input = '/route/*/ending/*' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route/.*/ending[/]{0,1}(?.*)' + $in = '/route/*/ending/*' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } It 'Update route slashes, with midpoint wildcard, ending with wildcard, no slash' { - $input = 'route/*/ending/*' - Update-PodeRouteSlashes -Path $input -Static | Should Be '/route/.*/ending[/]{0,1}(?.*)' + $in = 'route/*/ending/*' + Update-PodeRouteSlashes -Path $in -Static | Should -Be '/route/.*/ending[/]{0,1}(?.*)' } } Context 'Non Static' { It 'Update route slashes' { - $input = '/route' - Update-PodeRouteSlashes -Path $input | Should Be '/route' + $in = '/route' + Update-PodeRouteSlashes -Path $in | Should -Be '/route' } It 'Update route slashes, no slash' { - $input = 'route' - Update-PodeRouteSlashes -Path $input | Should Be '/route' + $in = 'route' + Update-PodeRouteSlashes -Path $in | Should -Be '/route' } It 'Update route slashes, ending with wildcard' { - $input = '/route/*' - Update-PodeRouteSlashes -Path $input | Should Be '/route/.*' + $in = '/route/*' + Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*' } It 'Update route slashes, ending with wildcard, no slash' { - $input = 'route/*' - Update-PodeRouteSlashes -Path $input | Should Be '/route/.*' + $in = 'route/*' + Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*' } It 'Update route slashes, with midpoint wildcard' { - $input = '/route/*/ending' - Update-PodeRouteSlashes -Path $input | Should Be '/route/.*/ending' + $in = '/route/*/ending' + Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending' } It 'Update route slashes, with midpoint wildcard, no slash' { - $input = 'route/*/ending' - Update-PodeRouteSlashes -Path $input | Should Be '/route/.*/ending' + $in = 'route/*/ending' + Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending' } It 'Update route slashes, with midpoint wildcard, ending with wildcard' { - $input = '/route/*/ending/*' - Update-PodeRouteSlashes -Path $input | Should Be '/route/.*/ending/.*' + $in = '/route/*/ending/*' + Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending/.*' } It 'Update route slashes, with midpoint wildcard, ending with wildcard, no slash' { - $input = 'route/*/ending/*' - Update-PodeRouteSlashes -Path $input | Should Be '/route/.*/ending/.*' + $in = 'route/*/ending/*' + Update-PodeRouteSlashes -Path $in | Should -Be '/route/.*/ending/.*' } } } Describe 'Resolve-PodePlaceholders' { It 'Update route placeholders, basic' { - $input = 'route' - Resolve-PodePlaceholders -Path $input | Should Be 'route' + $in = 'route' + Resolve-PodePlaceholders -Path $in | Should -Be 'route' } It 'Update route placeholders' { - $input = ':route' - Resolve-PodePlaceholders -Path $input | Should Be '(?[^\/]+?)' + $in = ':route' + Resolve-PodePlaceholders -Path $in | Should -Be '(?[^\/]+?)' } It 'Update route placeholders, double with no spacing' { - $input = ':route:placeholder' - Resolve-PodePlaceholders -Path $input | Should Be '(?[^\/]+?)(?[^\/]+?)' + $in = ':route:placeholder' + Resolve-PodePlaceholders -Path $in | Should -Be '(?[^\/]+?)(?[^\/]+?)' } It 'Update route placeholders, double with double ::' { - $input = '::route:placeholder' - Resolve-PodePlaceholders -Path $input | Should Be ':(?[^\/]+?)(?[^\/]+?)' + $in = '::route:placeholder' + Resolve-PodePlaceholders -Path $in | Should -Be ':(?[^\/]+?)(?[^\/]+?)' } It 'Update route placeholders, double with slash' { - $input = ':route/:placeholder' - Resolve-PodePlaceholders -Path $input | Should Be '(?[^\/]+?)/(?[^\/]+?)' + $in = ':route/:placeholder' + Resolve-PodePlaceholders -Path $in | Should -Be '(?[^\/]+?)/(?[^\/]+?)' } It 'Update route placeholders, no update' { - $input = ': route' - Resolve-PodePlaceholders -Path $input | Should Be ': route' + $in = ': route' + Resolve-PodePlaceholders -Path $in | Should -Be ': route' } } Describe 'Split-PodeRouteQuery' { It 'Split route, no split' { - $input = 'route' - Split-PodeRouteQuery -Path $input | Should Be 'route' + $in = 'route' + Split-PodeRouteQuery -Path $in | Should -Be 'route' } It 'Split route, split' { - $input = 'route?' - Split-PodeRouteQuery -Path $input | Should Be 'route' + $in = 'route?' + Split-PodeRouteQuery -Path $in | Should -Be 'route' } It 'Split route, split' { - $input = 'route?split' - Split-PodeRouteQuery -Path $input | Should Be 'route' + $in = 'route?split' + Split-PodeRouteQuery -Path $in | Should -Be 'route' } It 'Split route, split, first character' { - $input = '?route' - Split-PodeRouteQuery -Path $input | Should Be '' + $in = '?route' + Split-PodeRouteQuery -Path $in | Should -Be '' } } Describe 'Get-PodeRouteByUrl' { - $routeNameSet = @{ - Endpoint = @{ - Protocol = 'HTTP' - Address = '/assets' - Name = 'Example1' + BeforeEach { + $routeNameSet = @{ + Endpoint = @{ + Protocol = 'HTTP' + Address = '/assets' + Name = 'Example1' + } } - } - $routeNoNameSet = @{ - Endpoint = @{ - Protocol = '' - Address = '/assets' - Name = 'Example2' - } - } + $routeNoNameSet = @{ + Endpoint = @{ + Protocol = '' + Address = '/assets' + Name = 'Example2' + } + } } It 'Single route' { $Routes = @($routeNameSet) $Result = Get-PodeRouteByUrl -Routes $Routes -EndpointName 'Example1' - $Result | Should Not Be $null - $Result | Should Be $routeNameSet + $Result | Should -Not -Be $null + $Result | Should -Be $routeNameSet } It 'No routes' { @@ -795,7 +810,7 @@ Describe 'Get-PodeRouteByUrl' { $Result = Get-PodeRouteByUrl -Routes $Routes -EndpointName 'Example1' - $Result | Should Be $null + $Result | Should -Be $null } It 'Two routes, sorting' { @@ -803,67 +818,72 @@ Describe 'Get-PodeRouteByUrl' { $Result = Get-PodeRouteByUrl -Routes $Routes -EndpointName 'Example1' - $Result | Should Not Be $null - $Result | Should Be $routeNameSet + $Result | Should -Not -Be $null + $Result | Should -Be $routeNameSet } } Describe 'Get-PodeRoute' { - Mock Test-PodeIPAddress { return $true } - Mock Test-PodeIsAdminUser { return $true } + BeforeAll { + Mock Test-PodeIPAddress { return $true } + Mock Test-PodeIsAdminUser { return $true } } + BeforeEach { + $PodeContext.Server = @{ 'Routes' = @{ 'GET' = @{}; 'POST' = @{}; }; 'FindEndpoints' = @{}; 'Endpoints' = @{}; 'EndpointsMap' = @{}; 'Type' = $null + 'OpenAPI' = @{ + SelectedDefinitionTag = 'default' + Definitions = @{ + default = Get-PodeOABaseObject + } + } + } + } It 'Returns both routes whe nothing supplied' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = Get-PodeRoute - $routes.Length | Should Be 3 + $routes.Length | Should -Be 3 } It 'Returns both routes for GET method' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = Get-PodeRoute -Method Get - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } It 'Returns one route for POST method' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = Get-PodeRoute -Method Post - $routes.Length | Should Be 1 + $routes.Length | Should -Be 1 } It 'Returns both routes for users path' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = Get-PodeRoute -Path '/users' - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } It 'Returns one route for users path and GET method' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; FindEndpoints = @{} } Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Get -Path '/about' -ScriptBlock { Write-Host 'hello' } Add-PodeRoute -Method Post -Path '/users' -ScriptBlock { Write-Host 'hello' } $routes = Get-PodeRoute -Method Get -Path '/users' - $routes.Length | Should Be 1 + $routes.Length | Should -Be 1 } It 'Returns one route for users path and endpoint name user' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -872,13 +892,12 @@ Describe 'Get-PodeRoute' { Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName admin $routes = @(Get-PodeRoute -Method Get -Path '/users' -EndpointName user) - $routes.Length | Should Be 1 - $routes[0].Endpoint.Name | Should Be 'user' - $routes[0].Endpoint.Address | Should Be '127.0.0.1:8080' + $routes.Length | Should -Be 1 + $routes[0].Endpoint.Name | Should -Be 'user' + $routes[0].Endpoint.Address | Should -Be '127.0.0.1:8080' } It 'Returns both routes for users path and endpoint names' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -887,11 +906,10 @@ Describe 'Get-PodeRoute' { Add-PodeRoute -Method Get -Path '/users' -ScriptBlock { Write-Host 'hello' } -EndpointName admin $routes = @(Get-PodeRoute -Method Get -Path '/users' -EndpointName user, admin) - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } It 'Returns both routes for user endpoint name' { - $PodeContext.Server = @{ Routes = @{ GET = @{}; POST = @{}; }; Endpoints = @{}; EndpointsMap = @{}; FindEndpoints = @{}; Type = $null } Add-PodeEndpoint -Address '127.0.0.1' -Port 8080 -Protocol Http -Name user Add-PodeEndpoint -Address '127.0.0.1' -Port 8081 -Protocol Http -Name admin @@ -900,30 +918,32 @@ Describe 'Get-PodeRoute' { Add-PodeRoute -Method Get -Path '/users2' -ScriptBlock { Write-Host 'hello' } -EndpointName user, admin $routes = @(Get-PodeRoute -Method Get -EndpointName user) - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } } Describe 'Get-PodeStaticRoute' { - Mock Test-PodePath { return $true } - Mock New-PodePSDrive { return './assets' } - + BeforeAll { + Mock Test-PodePath { return $true } + Mock New-PodePSDrive { return './assets' } + } + BeforeEach { + $PodeContext.Server = @{ 'Routes' = @{ 'STATIC' = @{}; }; 'Root' = $pwd ; 'FindEndpoints' = @{}; 'Endpoints' = @{}; 'OpenAPI' = @{'default' = (Get-PodeOABaseObject) }; 'SelectedOADefinitionTag' = 'default' } + } It 'Returns all static routes' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; FindEndpoints = @{}; Root = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' Add-PodeStaticRoute -Path '/images' -Source './images' $routes = Get-PodeStaticRoute - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } It 'Returns one static route' { - $PodeContext.Server = @{ Routes = @{ STATIC = @{}; }; FindEndpoints = @{}; Root = $pwd } Add-PodeStaticRoute -Path '/assets' -Source './assets' Add-PodeStaticRoute -Path '/images' -Source './images' $routes = Get-PodeStaticRoute -Path '/images' - $routes.Length | Should Be 1 + $routes.Length | Should -Be 1 } It 'Returns one static route for endpoint name user' { @@ -936,9 +956,9 @@ Describe 'Get-PodeStaticRoute' { Add-PodeStaticRoute -Path '/images' -Source './images' -EndpointName admin $routes = @(Get-PodeStaticRoute -Path '/images' -EndpointName user) - $routes.Length | Should Be 1 - $routes[0].Endpoint.Name | Should Be 'user' - $routes[0].Endpoint.Address | Should Be '127.0.0.1:8080' + $routes.Length | Should -Be 1 + $routes[0].Endpoint.Name | Should -Be 'user' + $routes[0].Endpoint.Address | Should -Be '127.0.0.1:8080' } It 'Returns both routes for users path and endpoint names' { @@ -951,7 +971,7 @@ Describe 'Get-PodeStaticRoute' { Add-PodeStaticRoute -Path '/images' -Source './images' -EndpointName admin $routes = @(Get-PodeStaticRoute -Path '/images' -EndpointName user, admin) - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } It 'Returns both routes for user endpoint' { @@ -964,80 +984,88 @@ Describe 'Get-PodeStaticRoute' { Add-PodeStaticRoute -Path '/images2' -Source './images' -EndpointName user, admin $routes = @(Get-PodeStaticRoute -EndpointName user) - $routes.Length | Should Be 2 + $routes.Length | Should -Be 2 } } Describe 'Find-PodeRouteTransferEncoding' { It 'Returns nothing' { - Find-PodeRouteTransferEncoding -Path '/users' | Should Be ([string]::Empty) + Find-PodeRouteTransferEncoding -Path '/users' | Should -Be ([string]::Empty) } It 'Returns the passed encoding' { - Find-PodeRouteTransferEncoding -Path '/users' -TransferEncoding 'text/xml' | Should Be 'text/xml' + Find-PodeRouteTransferEncoding -Path '/users' -TransferEncoding 'text/xml' | Should -Be 'text/xml' } It 'Returns a default encoding' { $PodeContext.Server = @{ Web = @{ TransferEncoding = @{ Default = 'text/yml' } } } - Find-PodeRouteTransferEncoding -Path '/users' | Should Be 'text/yml' + Find-PodeRouteTransferEncoding -Path '/users' | Should -Be 'text/yml' } It 'Returns a path match' { $PodeContext.Server = @{ Web = @{ TransferEncoding = @{ Routes = @{ - '/users' = 'text/json' - } } } } + '/users' = 'text/json' + } + } + } + } - Find-PodeRouteTransferEncoding -Path '/users' | Should Be 'text/json' + Find-PodeRouteTransferEncoding -Path '/users' | Should -Be 'text/json' } } Describe 'Find-PodeRouteContentType' { It 'Returns nothing' { - Find-PodeRouteContentType -Path '/users' | Should Be ([string]::Empty) + Find-PodeRouteContentType -Path '/users' | Should -Be ([string]::Empty) } It 'Returns the passed type' { - Find-PodeRouteContentType -Path '/users' -ContentType 'text/xml' | Should Be 'text/xml' + Find-PodeRouteContentType -Path '/users' -ContentType 'text/xml' | Should -Be 'text/xml' } It 'Returns a default type' { $PodeContext.Server = @{ Web = @{ ContentType = @{ Default = 'text/yml' } } } - Find-PodeRouteContentType -Path '/users' | Should Be 'text/yml' + Find-PodeRouteContentType -Path '/users' | Should -Be 'text/yml' } It 'Returns a path match' { $PodeContext.Server = @{ Web = @{ ContentType = @{ Routes = @{ - '/users' = 'text/json' - } } } } + '/users' = 'text/json' + } + } + } + } - Find-PodeRouteContentType -Path '/users' | Should Be 'text/json' + Find-PodeRouteContentType -Path '/users' | Should -Be 'text/json' } } Describe 'ConvertTo-PodeMiddleware' { - $_PSSession = @{} + BeforeAll { + $_PSSession = @{} + } It 'Returns no middleware' { - @(ConvertTo-PodeMiddleware -PSSession $_PSSession) | Should Be $null + @(ConvertTo-PodeMiddleware -PSSession $_PSSession) | Should -Be $null } It 'Errors for invalid middleware type' { - { ConvertTo-PodeMiddleware -Middleware 'string' -PSSession $_PSSession } | Should Throw 'invalid type' + { ConvertTo-PodeMiddleware -Middleware 'string' -PSSession $_PSSession } | Should -Throw -ExpectedMessage '*invalid type*' } It 'Errors for invalid middleware hashtable - no logic' { - { ConvertTo-PodeMiddleware -Middleware @{} -PSSession $_PSSession } | Should Throw 'no logic defined' + { ConvertTo-PodeMiddleware -Middleware @{} -PSSession $_PSSession } | Should -Throw -ExpectedMessage '*no logic defined*' } It 'Errors for invalid middleware hashtable - logic not scriptblock' { - { ConvertTo-PodeMiddleware -Middleware @{ Logic = 'string' } -PSSession $_PSSession } | Should Throw 'invalid logic type' + { ConvertTo-PodeMiddleware -Middleware @{ Logic = 'string' } -PSSession $_PSSession } | Should -Throw -ExpectedMessage '*invalid logic type*' } It 'Returns hashtable for single hashtable middleware' { $middleware = @{ Logic = { Write-Host 'Hello' } } $converted = @(ConvertTo-PodeMiddleware -Middleware $middleware -PSSession $_PSSession) - $converted.Length | Should Be 1 - $converted[0].Logic.ToString() | Should Be ($middleware.Logic.ToString()) + $converted.Length | Should -Be 1 + $converted[0].Logic.ToString() | Should -Be ($middleware.Logic.ToString()) } It 'Returns hashtable for multiple hashtable middleware' { @@ -1046,16 +1074,16 @@ Describe 'ConvertTo-PodeMiddleware' { $converted = @(ConvertTo-PodeMiddleware -Middleware @($middleware1, $middleware2) -PSSession $_PSSession) - $converted.Length | Should Be 2 - $converted[0].Logic.ToString() | Should Be ($middleware1.Logic.ToString()) - $converted[1].Logic.ToString() | Should Be ($middleware2.Logic.ToString()) + $converted.Length | Should -Be 2 + $converted[0].Logic.ToString() | Should -Be ($middleware1.Logic.ToString()) + $converted[1].Logic.ToString() | Should -Be ($middleware2.Logic.ToString()) } It 'Converts single scriptblock middleware to hashtable' { $middleware = { Write-Host 'Hello' } $converted = @(ConvertTo-PodeMiddleware -Middleware $middleware -PSSession $_PSSession) - $converted.Length | Should Be 1 - $converted[0].Logic.ToString() | Should Be ($middleware.ToString()) + $converted.Length | Should -Be 1 + $converted[0].Logic.ToString() | Should -Be ($middleware.ToString()) } It 'Converts multiple scriptblock middleware to hashtable' { @@ -1064,9 +1092,9 @@ Describe 'ConvertTo-PodeMiddleware' { $converted = @(ConvertTo-PodeMiddleware -Middleware @($middleware1, $middleware2) -PSSession $_PSSession) - $converted.Length | Should Be 2 - $converted[0].Logic.ToString() | Should Be ($middleware1.ToString()) - $converted[1].Logic.ToString() | Should Be ($middleware2.ToString()) + $converted.Length | Should -Be 2 + $converted[0].Logic.ToString() | Should -Be ($middleware1.ToString()) + $converted[1].Logic.ToString() | Should -Be ($middleware2.ToString()) } It 'Handles a mixture of hashtable and scriptblock' { @@ -1075,8 +1103,8 @@ Describe 'ConvertTo-PodeMiddleware' { $converted = @(ConvertTo-PodeMiddleware -Middleware @($middleware1, $middleware2) -PSSession $_PSSession) - $converted.Length | Should Be 2 - $converted[0].Logic.ToString() | Should Be ($middleware1.Logic.ToString()) - $converted[1].Logic.ToString() | Should Be ($middleware2.ToString()) + $converted.Length | Should -Be 2 + $converted[0].Logic.ToString() | Should -Be ($middleware1.Logic.ToString()) + $converted[1].Logic.ToString() | Should -Be ($middleware2.ToString()) } } \ No newline at end of file diff --git a/tests/unit/Schedules.Tests.ps1 b/tests/unit/Schedules.Tests.ps1 index 373dc01bf..05e688db1 100644 --- a/tests/unit/Schedules.Tests.ps1 +++ b/tests/unit/Schedules.Tests.ps1 @@ -1,54 +1,59 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Find-PodeSchedule' { Context 'Invalid parameters supplied' { It 'Throw null name parameter error' { - { Find-PodeSchedule -Name $null } | Should Throw 'The argument is null or empty' + { Find-PodeSchedule -Name $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } It 'Throw empty name parameter error' { - { Find-PodeSchedule -Name ([string]::Empty) } | Should Throw 'The argument is null or empty' + { Find-PodeSchedule -Name ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } } Context 'Valid values supplied' { It 'Returns null as the schedule does not exist' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } - Find-PodeSchedule -Name 'test' | Should Be $null + Find-PodeSchedule -Name 'test' | Should -Be $null } It 'Returns schedule for name' { $PodeContext = @{ 'Schedules' = @{ Items = @{ 'test' = @{ 'Name' = 'test'; }; } }; } $result = (Find-PodeSchedule -Name 'test') - $result | Should BeOfType System.Collections.Hashtable - $result.Name | Should Be 'test' + $result | Should -BeOfType System.Collections.Hashtable + $result.Name | Should -Be 'test' } } } Describe 'Add-PodeSchedule' { - Mock 'ConvertFrom-PodeCronExpression' { @{} } - Mock 'Get-PodeCronNextEarliestTrigger' { [datetime]::new(2020, 1, 1) } - + BeforeAll { + Mock 'ConvertFrom-PodeCronExpression' { @{} } + Mock 'Get-PodeCronNextEarliestTrigger' { [datetime]::new(2020, 1, 1) } + } It 'Throws error because schedule already exists' { $PodeContext = @{ 'Schedules' = @{ Items = @{ 'test' = $null }; } } - { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} } | Should Throw 'already defined' + { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already defined*' } It 'Throws error because end time in the past' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } $end = ([DateTime]::Now.AddHours(-1)) - { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -EndTime $end } | Should Throw 'the EndTime value must be in the future' + { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -EndTime $end } | Should -Throw -ExpectedMessage '*the EndTime value must be in the future*' } It 'Throws error because start time is after end time' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } $start = ([DateTime]::Now.AddHours(3)) $end = ([DateTime]::Now.AddHours(1)) - { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -StartTime $start -EndTime $end } | Should Throw 'starttime after the endtime' + { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock {} -StartTime $start -EndTime $end } | Should -Throw -ExpectedMessage '*starttime after the endtime*' } It 'Adds new schedule supplying everything' { @@ -59,13 +64,13 @@ Describe 'Add-PodeSchedule' { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedule = $PodeContext.Schedules.Items['test'] - $schedule | Should Not Be $null - $schedule.Name | Should Be 'test' - $schedule.StartTime | Should Be $start - $schedule.EndTime | Should Be $end - $schedule.Script | Should Not Be $null - $schedule.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $schedule.Crons.Length | Should Be 1 + $schedule | Should -Not -Be $null + $schedule.Name | Should -Be 'test' + $schedule.StartTime | Should -Be $start + $schedule.EndTime | Should -Be $end + $schedule.Script | Should -Not -Be $null + $schedule.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $schedule.Crons.Length | Should -Be 1 } It 'Adds new schedule with no start time' { @@ -75,13 +80,13 @@ Describe 'Add-PodeSchedule' { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -EndTime $end $schedule = $PodeContext.Schedules.Items['test'] - $schedule | Should Not Be $null - $schedule.Name | Should Be 'test' - $schedule.StartTime | Should Be $null - $schedule.EndTime | Should Be $end - $schedule.Script | Should Not Be $null - $schedule.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $schedule.Crons.Length | Should Be 1 + $schedule | Should -Not -Be $null + $schedule.Name | Should -Be 'test' + $schedule.StartTime | Should -Be $null + $schedule.EndTime | Should -Be $end + $schedule.Script | Should -Not -Be $null + $schedule.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $schedule.Crons.Length | Should -Be 1 } It 'Adds new schedule with no end time' { @@ -91,13 +96,13 @@ Describe 'Add-PodeSchedule' { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start $schedule = $PodeContext.Schedules.Items['test'] - $schedule | Should Not Be $null - $schedule.Name | Should Be 'test' - $schedule.StartTime | Should Be $start - $schedule.EndTime | Should Be $null - $schedule.Script | Should Not Be $null - $schedule.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $schedule.Crons.Length | Should Be 1 + $schedule | Should -Not -Be $null + $schedule.Name | Should -Be 'test' + $schedule.StartTime | Should -Be $start + $schedule.EndTime | Should -Be $null + $schedule.Script | Should -Not -Be $null + $schedule.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $schedule.Crons.Length | Should -Be 1 } It 'Adds new schedule with just a cron' { @@ -106,13 +111,13 @@ Describe 'Add-PodeSchedule' { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } $schedule = $PodeContext.Schedules.Items['test'] - $schedule | Should Not Be $null - $schedule.Name | Should Be 'test' - $schedule.StartTime | Should Be $null - $schedule.EndTime | Should Be $null - $schedule.Script | Should Not Be $null - $schedule.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $schedule.Crons.Length | Should Be 1 + $schedule | Should -Not -Be $null + $schedule.Name | Should -Be 'test' + $schedule.StartTime | Should -Be $null + $schedule.EndTime | Should -Be $null + $schedule.Script | Should -Not -Be $null + $schedule.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $schedule.Crons.Length | Should -Be 1 } It 'Adds new schedule with two crons' { @@ -123,13 +128,13 @@ Describe 'Add-PodeSchedule' { Add-PodeSchedule -Name 'test' -Cron @('@minutely', '@hourly') -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedule = $PodeContext.Schedules.Items['test'] - $schedule | Should Not Be $null - $schedule.Name | Should Be 'test' - $schedule.StartTime | Should Be $start - $schedule.EndTime | Should Be $end - $schedule.Script | Should Not Be $null - $schedule.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() - $schedule.Crons.Length | Should Be 2 + $schedule | Should -Not -Be $null + $schedule.Name | Should -Be 'test' + $schedule.StartTime | Should -Be $start + $schedule.EndTime | Should -Be $end + $schedule.Script | Should -Not -Be $null + $schedule.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() + $schedule.Crons.Length | Should -Be 2 } } @@ -137,7 +142,7 @@ Describe 'Get-PodeSchedule' { It 'Returns no schedules' { $PodeContext = @{ Schedules = @{ Items = @{} } } $schedules = Get-PodeSchedule - $schedules.Length | Should Be 0 + $schedules.Length | Should -Be 0 } It 'Returns 1 schedule by name' { @@ -147,12 +152,12 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule - $schedules.Length | Should Be 1 + $schedules.Length | Should -Be 1 - $schedules.Name | Should Be 'test1' - $schedules.StartTime | Should Be $start - $schedules.EndTime | Should Be $end - $schedules.Limit | Should Be 0 + $schedules.Name | Should -Be 'test1' + $schedules.StartTime | Should -Be $start + $schedules.EndTime | Should -Be $end + $schedules.Limit | Should -Be 0 } It 'Returns 1 schedule by start time' { @@ -162,12 +167,12 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule -StartTime $start.AddHours(1) - $schedules.Length | Should Be 1 + $schedules.Length | Should -Be 1 - $schedules.Name | Should Be 'test1' - $schedules.StartTime | Should Be $start - $schedules.EndTime | Should Be $end - $schedules.Limit | Should Be 0 + $schedules.Name | Should -Be 'test1' + $schedules.StartTime | Should -Be $start + $schedules.EndTime | Should -Be $end + $schedules.Limit | Should -Be 0 } It 'Returns 1 schedule by end time' { @@ -177,12 +182,12 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule -EndTime $end - $schedules.Length | Should Be 1 + $schedules.Length | Should -Be 1 - $schedules.Name | Should Be 'test1' - $schedules.StartTime | Should Be $start - $schedules.EndTime | Should Be $end - $schedules.Limit | Should Be 0 + $schedules.Name | Should -Be 'test1' + $schedules.StartTime | Should -Be $start + $schedules.EndTime | Should -Be $end + $schedules.Limit | Should -Be 0 } It 'Returns 1 schedule by both start and end time' { @@ -192,12 +197,12 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule -StartTime $start.AddHours(1) -EndTime $end - $schedules.Length | Should Be 1 + $schedules.Length | Should -Be 1 - $schedules.Name | Should Be 'test1' - $schedules.StartTime | Should Be $start - $schedules.EndTime | Should Be $end - $schedules.Limit | Should Be 0 + $schedules.Name | Should -Be 'test1' + $schedules.StartTime | Should -Be $start + $schedules.EndTime | Should -Be $end + $schedules.Limit | Should -Be 0 } It 'Returns no schedules by end time before start' { @@ -207,7 +212,7 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule -EndTime $start.AddHours(-1) - $schedules.Length | Should Be 0 + $schedules.Length | Should -Be 0 } It 'Returns no schedules by start time after end' { @@ -217,7 +222,7 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule -StartTime $end.AddHours(1) - $schedules.Length | Should Be 0 + $schedules.Length | Should -Be 0 } It 'Returns 2 schedules by name' { @@ -230,7 +235,7 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test3' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule -Name test1, test2 - $schedules.Length | Should Be 2 + $schedules.Length | Should -Be 2 } It 'Returns all schedules' { @@ -243,7 +248,7 @@ Describe 'Get-PodeSchedule' { Add-PodeSchedule -Name 'test3' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } -StartTime $start -EndTime $end $schedules = Get-PodeSchedule - $schedules.Length | Should Be 3 + $schedules.Length | Should -Be 3 } } @@ -258,7 +263,7 @@ Describe 'Get-PodeScheduleNextTrigger' { $expected = $start.AddHours(1) $expected = [datetime]::new($expected.Year, $expected.Month, $expected.Day, $expected.Hour, 0, 0) - $trigger | Should Be $expected + $trigger | Should -Be $expected } It 'Returns next trigger time from date' { @@ -271,7 +276,7 @@ Describe 'Get-PodeScheduleNextTrigger' { $expected = $start.AddHours(2) $expected = [datetime]::new($expected.Year, $expected.Month, $expected.Day, $expected.Hour, 0, 0) - $trigger | Should Be $expected + $trigger | Should -Be $expected } } @@ -281,11 +286,11 @@ Describe 'Remove-PodeSchedule' { Add-PodeSchedule -Name 'test' -Cron '@hourly' -ScriptBlock { Write-Host 'hello' } - $PodeContext.Schedules.Items['test'] | Should Not Be $null + $PodeContext.Schedules.Items['test'] | Should -Not -Be $null Remove-PodeSchedule -Name 'test' - $PodeContext.Schedules.Items['test'] | Should Be $null + $PodeContext.Schedules.Items['test'] | Should -Be $null } } @@ -297,12 +302,12 @@ Describe 'Clear-PodeSchedules' { Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello1' } Add-PodeSchedule -Name 'test2' -Cron '@hourly' -ScriptBlock { Write-Host 'hello2' } - $PodeContext.Schedules.Items['test1'] | Should Not Be $null - $PodeContext.Schedules.Items['test2'] | Should Not Be $null + $PodeContext.Schedules.Items['test1'] | Should -Not -Be $null + $PodeContext.Schedules.Items['test2'] | Should -Not -Be $null Clear-PodeSchedules - $PodeContext.Schedules.Items.Count | Should Be 0 + $PodeContext.Schedules.Items.Count | Should -Be 0 } } @@ -310,22 +315,22 @@ Describe 'Edit-PodeSchedule' { It 'Adds a new schedule, then edits the cron' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello1' } - $PodeContext.Schedules.Items['test1'].Crons.Length | Should Be 1 - $PodeContext.Schedules.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() + $PodeContext.Schedules.Items['test1'].Crons.Length | Should -Be 1 + $PodeContext.Schedules.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() Edit-PodeSchedule -Name 'test1' -Cron @('@minutely', '@hourly') - $PodeContext.Schedules.Items['test1'].Crons.Length | Should Be 2 - $PodeContext.Schedules.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() + $PodeContext.Schedules.Items['test1'].Crons.Length | Should -Be 2 + $PodeContext.Schedules.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() } It 'Adds a new schedule, then edits the script' { $PodeContext = @{ 'Schedules' = @{ Items = @{} }; } Add-PodeSchedule -Name 'test1' -Cron '@hourly' -ScriptBlock { Write-Host 'hello1' } - $PodeContext.Schedules.Items['test1'].Crons.Length | Should Be 1 - $PodeContext.Schedules.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() + $PodeContext.Schedules.Items['test1'].Crons.Length | Should -Be 1 + $PodeContext.Schedules.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() Edit-PodeSchedule -Name 'test1' -ScriptBlock { Write-Host 'hello2' } - $PodeContext.Schedules.Items['test1'].Crons.Length | Should Be 1 - $PodeContext.Schedules.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello2' }).ToString() + $PodeContext.Schedules.Items['test1'].Crons.Length | Should -Be 1 + $PodeContext.Schedules.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello2' }).ToString() } } \ No newline at end of file diff --git a/tests/unit/Security.Tests.ps1 b/tests/unit/Security.Tests.ps1 index 559d7358d..9862b9510 100644 --- a/tests/unit/Security.Tests.ps1 +++ b/tests/unit/Security.Tests.ps1 @@ -1,13 +1,17 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() -$PodeContext = @{ 'Server' = $null; } +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + $PodeContext = @{ 'Server' = $null; } +} Describe 'Test-PodeIPAccess' { Context 'Invalid parameters' { It 'Throws error for invalid IP' { - { Test-PodeIPAccess -IP $null -Limit 1 -Seconds 1 } | Should Throw "argument is null" + { Test-PodeIPAccess -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*argument is null*' } } } @@ -15,13 +19,14 @@ Describe 'Test-PodeIPAccess' { Describe 'Test-PodeIPLimit' { Context 'Invalid parameters' { It 'Throws error for invalid IP' { - { Test-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should Throw "argument is null" + { Test-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*argument is null*' } } } Describe 'Add-PodeLimitRule' { - Mock Add-PodeIPLimit { } + BeforeAll { + Mock Add-PodeIPLimit { } } Context 'Valid parameters' { It 'Adds single IP address' { @@ -47,8 +52,9 @@ Describe 'Add-PodeLimitRule' { } Describe 'Add-PodeAccessRule' { - Mock Add-PodeIPAccess { } - + BeforeAll { + Mock Add-PodeIPAccess { } + } Context 'Valid parameters' { It 'Adds single IP address' { Add-PodeAccessRule -Access 'Allow' -Type 'IP' -Values '127.0.0.1' @@ -75,23 +81,23 @@ Describe 'Add-PodeAccessRule' { Describe 'Add-PodeIPLimit' { Context 'Invalid parameters' { It 'Throws error for invalid IP' { - { Add-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should Throw "because it is an empty string" + { Add-PodeIPLimit -IP $null -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*because it is an empty string*' } It 'Throws error for negative limit' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit -1 -Seconds 1 } | Should Throw '0 or less' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit -1 -Seconds 1 } | Should -Throw -ExpectedMessage '*0 or less*' } It 'Throws error for negative seconds' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds -1 } | Should Throw '0 or less' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds -1 } | Should -Throw -ExpectedMessage '*0 or less*' } It 'Throws error for zero limit' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit 0 -Seconds 1 } | Should Throw '0 or less' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit 0 -Seconds 1 } | Should -Throw -ExpectedMessage '*0 or less*' } It 'Throws error for zero seconds' { - { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds 0 } | Should Throw '0 or less' + { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds 0 } | Should -Throw -ExpectedMessage '*0 or less*' } } @@ -101,21 +107,21 @@ Describe 'Add-PodeIPLimit' { Add-PodeIPLimit -IP '127.0.0.1' -Limit 1 -Seconds 1 $a = $PodeContext.Server.Limits.Rules.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true $k = $a['127.0.0.1'] - $k.Limit | Should Be 1 - $k.Seconds | Should Be 1 + $k.Limit | Should -Be 1 + $k.Seconds | Should -Be 1 - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(127, 0, 0, 1) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(127, 0, 0, 1) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(127, 0, 0, 1) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(127, 0, 0, 1) } It 'Adds any IP to limit' { @@ -123,21 +129,21 @@ Describe 'Add-PodeIPLimit' { Add-PodeIPLimit -IP 'all' -Limit 1 -Seconds 1 $a = $PodeContext.Server.Limits.Rules.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('all') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('all') | Should -Be $true $k = $a['all'] - $k.Limit | Should Be 1 - $k.Seconds | Should Be 1 + $k.Limit | Should -Be 1 + $k.Seconds | Should -Be 1 - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(0, 0, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(0, 0, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(255, 255, 255, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(255, 255, 255, 255) } It 'Adds a subnet mask to limit' { @@ -145,22 +151,22 @@ Describe 'Add-PodeIPLimit' { Add-PodeIPLimit -IP '10.10.0.0/24' -Limit 1 -Seconds 1 $a = $PodeContext.Server.Limits.Rules.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('10.10.0.0/24') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('10.10.0.0/24') | Should -Be $true $k = $a['10.10.0.0/24'] - $k.Limit | Should Be 1 - $k.Seconds | Should Be 1 - $k.Grouped | Should Be $false + $k.Limit | Should -Be 1 + $k.Seconds | Should -Be 1 + $k.Grouped | Should -Be $false - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(10, 10, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(10, 10, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(10, 10, 0, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(10, 10, 0, 255) } It 'Adds a grouped subnet mask to limit' { @@ -168,27 +174,27 @@ Describe 'Add-PodeIPLimit' { Add-PodeIPLimit -IP '10.10.0.0/24' -Limit 1 -Seconds 1 -Group $a = $PodeContext.Server.Limits.Rules.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('10.10.0.0/24') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('10.10.0.0/24') | Should -Be $true $k = $a['10.10.0.0/24'] - $k.Limit | Should Be 1 - $k.Seconds | Should Be 1 - $k.Grouped | Should Be $true + $k.Limit | Should -Be 1 + $k.Seconds | Should -Be 1 + $k.Grouped | Should -Be $true - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(10, 10, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(10, 10, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(10, 10, 0, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(10, 10, 0, 255) } It 'Throws error for invalid IP' { $PodeContext.Server = @{ 'Limits' = @{ 'Rules' = @{}; 'Active' = @{}; } } - { Add-PodeIPLimit -IP '256.0.0.0' -Limit 1 -Seconds 1 } | Should Throw 'invalid ip address' + { Add-PodeIPLimit -IP '256.0.0.0' -Limit 1 -Seconds 1 } | Should -Throw -ExpectedMessage '*invalid ip address*' } } } @@ -200,18 +206,18 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Allow' -IP '127.0.0.1' $a = $PodeContext.Server.Access.Allow.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true $k = $a['127.0.0.1'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(127, 0, 0, 1) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(127, 0, 0, 1) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(127, 0, 0, 1) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(127, 0, 0, 1) } It 'Adds any IP to allow' { @@ -219,18 +225,18 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Allow' -IP 'all' $a = $PodeContext.Server.Access.Allow.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('all') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('all') | Should -Be $true $k = $a['all'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(0, 0, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(0, 0, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(255, 255, 255, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(255, 255, 255, 255) } It 'Adds a subnet mask to allow' { @@ -238,18 +244,18 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Allow' -IP '10.10.0.0/24' $a = $PodeContext.Server.Access.Allow.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('10.10.0.0/24') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('10.10.0.0/24') | Should -Be $true $k = $a['10.10.0.0/24'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(10, 10, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(10, 10, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(10, 10, 0, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(10, 10, 0, 255) } It 'Adds an IP to deny' { @@ -257,18 +263,18 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Deny' -IP '127.0.0.1' $a = $PodeContext.Server.Access.Deny.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true $k = $a['127.0.0.1'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(127, 0, 0, 1) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(127, 0, 0, 1) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(127, 0, 0, 1) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(127, 0, 0, 1) } It 'Adds any IP to deny' { @@ -276,18 +282,18 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Deny' -IP 'all' $a = $PodeContext.Server.Access.Deny.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('all') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('all') | Should -Be $true $k = $a['all'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(0, 0, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(0, 0, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(255, 255, 255, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(255, 255, 255, 255) } It 'Adds a subnet mask to deny' { @@ -295,18 +301,18 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Deny' -IP '10.10.0.0/24' $a = $PodeContext.Server.Access.Deny.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('10.10.0.0/24') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('10.10.0.0/24') | Should -Be $true $k = $a['10.10.0.0/24'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(10, 10, 0, 0) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(10, 10, 0, 0) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(10, 10, 0, 255) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(10, 10, 0, 255) } It 'Adds an IP to allow and removes one from deny' { @@ -316,33 +322,33 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Deny' -IP '127.0.0.1' $a = $PodeContext.Server.Access.Deny.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true - # add to allow, deny should be removed + # add to allow, deny Should -Be removed Add-PodeIPAccess -Access 'Allow' -IP '127.0.0.1' # check allow $a = $PodeContext.Server.Access.Allow.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true $k = $a['127.0.0.1'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(127, 0, 0, 1) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(127, 0, 0, 1) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(127, 0, 0, 1) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(127, 0, 0, 1) # check deny $a = $PodeContext.Server.Access.Deny.IP - $a | Should Not Be $null - $a.Count | Should Be 0 - $a.ContainsKey('127.0.0.1') | Should Be $false + $a | Should -Not -Be $null + $a.Count | Should -Be 0 + $a.ContainsKey('127.0.0.1') | Should -Be $false } It 'Adds an IP to deny and removes one from allow' { @@ -352,38 +358,38 @@ Describe 'Add-PodeIPAccess' { Add-PodeIPAccess -Access 'Allow' -IP '127.0.0.1' $a = $PodeContext.Server.Access.Allow.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true - # add to deny, allow should be removed + # add to deny, allow Should -Be removed Add-PodeIPAccess -Access 'Deny' -IP '127.0.0.1' # check deny $a = $PodeContext.Server.Access.Deny.IP - $a | Should Not Be $null - $a.Count | Should Be 1 - $a.ContainsKey('127.0.0.1') | Should Be $true + $a | Should -Not -Be $null + $a.Count | Should -Be 1 + $a.ContainsKey('127.0.0.1') | Should -Be $true $k = $a['127.0.0.1'] - $k.Lower | Should Not Be $null - $k.Lower.Family | Should Be 'InterNetwork' - $k.Lower.Bytes | Should Be @(127, 0, 0, 1) + $k.Lower | Should -Not -Be $null + $k.Lower.Family | Should -Be 'InterNetwork' + $k.Lower.Bytes | Should -Be @(127, 0, 0, 1) - $k.Upper | Should Not Be $null - $k.Upper.Family | Should Be 'InterNetwork' - $k.Upper.Bytes | Should Be @(127, 0, 0, 1) + $k.Upper | Should -Not -Be $null + $k.Upper.Family | Should -Be 'InterNetwork' + $k.Upper.Bytes | Should -Be @(127, 0, 0, 1) # check allow $a = $PodeContext.Server.Access.Allow.IP - $a | Should Not Be $null - $a.Count | Should Be 0 - $a.ContainsKey('127.0.0.1') | Should Be $false + $a | Should -Not -Be $null + $a.Count | Should -Be 0 + $a.ContainsKey('127.0.0.1') | Should -Be $false } It 'Throws error for invalid IP' { $PodeContext.Server = @{ 'Access' = @{ 'Allow' = @{}; 'Deny' = @{}; } } - { Add-PodeIPAccess -Access 'Allow' -IP '256.0.0.0' } | Should Throw 'invalid ip address' + { Add-PodeIPAccess -Access 'Allow' -IP '256.0.0.0' } | Should -Throw -ExpectedMessage '*invalid ip address*' } } } @@ -406,7 +412,7 @@ Describe 'Get-PodeCsrfMiddleware' { Mock Test-PodeCsrfConfigured { return $true } Mock New-PodeMiddleware { return { write-host 'hello' } } - (Get-PodeCsrfMiddleware).ToString() | Should Be ({ write-host 'hello' }).ToString() + (Get-PodeCsrfMiddleware).ToString() | Should -Be ({ write-host 'hello' }).ToString() } } @@ -416,15 +422,17 @@ Describe 'New-PodeCsrfToken' { Mock New-PodeCsrfSecret { return 'secret' } Mock New-PodeSalt { return 'salt' } Mock Invoke-PodeSHA256Hash { return 'salt-secret' } - New-PodeCsrfToken | Should Be 't:salt.salt-secret' + New-PodeCsrfToken | Should -Be 't:salt.salt-secret' } } Describe 'Initialize-PodeCsrf' { It 'Runs csrf setup using sessions' { $PodeContext = @{ 'Server' = @{ 'Cookies' = @{ - 'Csrf' = @{ 'Name' = 'Key' } - }}} + 'Csrf' = @{ 'Name' = 'Key' } + } + } + } Mock Test-PodeCsrfConfigured { return $false } Mock Test-PodeSessionsEnabled { return $true } @@ -432,16 +440,18 @@ Describe 'Initialize-PodeCsrf' { Initialize-PodeCsrf -IgnoreMethods @('Get') - $PodeContext.Server.Cookies.Csrf.Name | Should Be 'pode.csrf' - $PodeContext.Server.Cookies.Csrf.UseCookies | Should Be $false - $PodeContext.Server.Cookies.Csrf.Secret | Should Be '' - $PodeContext.Server.Cookies.Csrf.IgnoredMethods | Should Be @('Get') + $PodeContext.Server.Cookies.Csrf.Name | Should -Be 'pode.csrf' + $PodeContext.Server.Cookies.Csrf.UseCookies | Should -Be $false + $PodeContext.Server.Cookies.Csrf.Secret | Should -Be '' + $PodeContext.Server.Cookies.Csrf.IgnoredMethods | Should -Be @('Get') } It 'Runs csrf setup using cookies' { $PodeContext = @{ 'Server' = @{ 'Cookies' = @{ - 'Csrf' = @{ 'Name' = 'Key' } - }}} + 'Csrf' = @{ 'Name' = 'Key' } + } + } + } Mock Test-PodeCsrfConfigured { return $false } Mock Test-PodeSessionsEnabled { return $false } @@ -449,158 +459,176 @@ Describe 'Initialize-PodeCsrf' { Initialize-PodeCsrf -IgnoreMethods @('Get') -UseCookies - $PodeContext.Server.Cookies.Csrf.Name | Should Be 'pode.csrf' - $PodeContext.Server.Cookies.Csrf.UseCookies | Should Be $true - $PodeContext.Server.Cookies.Csrf.Secret | Should Be 'secret' - $PodeContext.Server.Cookies.Csrf.IgnoredMethods | Should Be @('Get') + $PodeContext.Server.Cookies.Csrf.Name | Should -Be 'pode.csrf' + $PodeContext.Server.Cookies.Csrf.UseCookies | Should -Be $true + $PodeContext.Server.Cookies.Csrf.Secret | Should -Be 'secret' + $PodeContext.Server.Cookies.Csrf.IgnoredMethods | Should -Be @('Get') } } Describe 'Get-PodeCsrfToken' { It 'Returns the token from the payload' { $PodeContext = @{ 'Server' = @{ 'Cookies' = @{ - 'Csrf' = @{ 'Name' = 'Key' } - }}} + 'Csrf' = @{ 'Name' = 'Key' } + } + } + } $WebEvent = @{ 'Data' = @{ - 'Key' = 'Token' - }} + 'Key' = 'Token' + } + } - Get-PodeCsrfToken | Should Be 'Token' + Get-PodeCsrfToken | Should -Be 'Token' } It 'Returns the token from the query string' { $PodeContext = @{ 'Server' = @{ 'Cookies' = @{ - 'Csrf' = @{ 'Name' = 'Key' } - }}} + 'Csrf' = @{ 'Name' = 'Key' } + } + } + } $WebEvent = @{ - 'Data' = @{}; - 'Query' = @{ 'Key' = 'Token' }; + 'Data' = @{} + 'Query' = @{ 'Key' = 'Token' } } - Get-PodeCsrfToken | Should Be 'Token' + Get-PodeCsrfToken | Should -Be 'Token' } It 'Returns the token from the headers' { $PodeContext = @{ 'Server' = @{ 'Cookies' = @{ - 'Csrf' = @{ 'Name' = 'Key' } - }}} + 'Csrf' = @{ 'Name' = 'Key' } + } + } + } $WebEvent = @{ - 'Data' = @{}; - 'Query' = @{}; + 'Data' = @{} + 'Query' = @{} 'Request' = @{ 'Headers' = @{ 'Key' = 'Token' } } } - Get-PodeCsrfToken | Should Be 'Token' + Get-PodeCsrfToken | Should -Be 'Token' } It 'Returns no token' { $PodeContext = @{ 'Server' = @{ 'Cookies' = @{ - 'Csrf' = @{ 'Name' = 'Key' } - }}} + 'Csrf' = @{ 'Name' = 'Key' } + } + } + } $WebEvent = @{ - 'Data' = @{}; - 'Query' = @{}; + 'Data' = @{} + 'Query' = @{} 'Request' = @{ 'Headers' = @{} } } - Get-PodeCsrfToken | Should Be $null + Get-PodeCsrfToken | Should -Be $null } } Describe 'Test-PodeCsrfToken' { It 'Returns false for no secret' { - Test-PodeCsrfToken -Secret '' -Token 'value' | Should Be $false + Test-PodeCsrfToken -Secret '' -Token 'value' | Should -Be $false } It 'Returns false for no token' { - Test-PodeCsrfToken -Secret 'key' -Token '' | Should Be $false + Test-PodeCsrfToken -Secret 'key' -Token '' | Should -Be $false } It 'Returns false for no tag on token' { - Test-PodeCsrfToken -Secret 'key' -Token 'value' | Should Be $false + Test-PodeCsrfToken -Secret 'key' -Token 'value' | Should -Be $false } It 'Returns false for no period in token' { - Test-PodeCsrfToken -Secret 'key' -Token 't:value' | Should Be $false + Test-PodeCsrfToken -Secret 'key' -Token 't:value' | Should -Be $false } It 'Returns false for token mismatch' { Mock New-PodeCsrfToken { return 'value2' } - Test-PodeCsrfToken -Secret 'key' -Token 't:value1.signed' | Should Be $false + Test-PodeCsrfToken -Secret 'key' -Token 't:value1.signed' | Should -Be $false } It 'Returns true for token match' { Mock Restore-PodeCsrfToken { return 't:value1.signed' } - Test-PodeCsrfToken -Secret 'key' -Token 't:value1.signed' | Should Be $true + Test-PodeCsrfToken -Secret 'key' -Token 't:value1.signed' | Should -Be $true } } Describe 'New-PodeCsrfSecret' { It 'Returns an existing secret' { Mock Get-PodeCsrfSecret { return 'key' } - New-PodeCsrfSecret | Should Be 'key' + New-PodeCsrfSecret | Should -Be 'key' } It 'Returns a new secret' { Mock Get-PodeCsrfSecret { return '' } Mock New-PodeGuid { return 'new-key' } Mock Set-PodeCsrfSecret { } - New-PodeCsrfSecret | Should Be 'new-key' + New-PodeCsrfSecret | Should -Be 'new-key' } } Describe 'New-PodeCsrfToken' { + It 'Throws error for csrf not being configured' { $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Csrf' = $null } - }} + 'Cookies' = @{ 'Csrf' = $null } + } + } - { New-PodeCsrfToken } | Should Throw 'not been initialised' + { New-PodeCsrfToken } | Should -Throw -ExpectedMessage '*not been initialised*' } - Mock Invoke-PodeSHA256Hash { return "$($Value)" } - $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Csrf' = @{ 'key' = 'value' } } - }} It 'Returns a token for new secret/salt' { + Mock Invoke-PodeSHA256Hash { return "$($Value)" } + + $PodeContext = @{ 'Server' = @{ + 'Cookies' = @{ 'Csrf' = @{ 'key' = 'value' } } + } + } Mock New-PodeCsrfSecret { return 'new-key' } Mock New-PodeSalt { return 'new-salt' } - New-PodeCsrfToken | Should Be 't:new-salt.new-salt-new-key' + New-PodeCsrfToken | Should -Be 't:new-salt.new-salt-new-key' } } Describe 'Restore-PodeCsrfToken' { - Mock Invoke-PodeSHA256Hash { return "$($Value)" } + BeforeAll { Mock Invoke-PodeSHA256Hash { return "$($Value)" } - $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Csrf' = @{ 'key' = 'value' } } - }} + $PodeContext = @{ 'Server' = @{ + 'Cookies' = @{ 'Csrf' = @{ 'key' = 'value' } } + } + } } It 'Returns a token for an existing secret/salt' { - Restore-PodeCsrfToken -Secret 'key' -Salt 'salt' | Should Be 't:salt.salt-key' + + Restore-PodeCsrfToken -Secret 'key' -Salt 'salt' | Should -Be 't:salt.salt-key' } } Describe 'Set-PodeCsrfSecret' { - $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Csrf' = @{ 'Name' = 'pode.csrf' } } - }} + BeforeAll { + $PodeContext = @{ 'Server' = @{ + 'Cookies' = @{ 'Csrf' = @{ 'Name' = 'pode.csrf' } } + } + } } It 'Sets the secret agaisnt the session' { $PodeContext.Server.Cookies.Csrf.UseCookies = $false $WebEvent = @{ 'Session' = @{ - 'Data' = @{} - } } + 'Data' = @{} + } + } Set-PodeCsrfSecret -Secret 'some-secret' - $WebEvent.Session.Data['pode.csrf'] | Should Be 'some-secret' + $WebEvent.Session.Data['pode.csrf'] | Should -Be 'some-secret' } It 'Sets the secret agaisnt a cookie' { @@ -614,24 +642,27 @@ Describe 'Set-PodeCsrfSecret' { } Describe 'Get-PodeCsrfSecret' { - $PodeContext = @{ 'Server' = @{ - 'Cookies' = @{ 'Csrf' = @{ 'Name' = 'pode.csrf' } } - }} + BeforeAll { + $PodeContext = @{ 'Server' = @{ + 'Cookies' = @{ 'Csrf' = @{ 'Name' = 'pode.csrf' } } + } + } } It 'Gets the secret from the session' { $PodeContext.Server.Cookies.Csrf.UseCookies = $false $WebEvent = @{ 'Session' = @{ - 'Data' = @{ 'pode.csrf' = 'some-secret' } - } } + 'Data' = @{ 'pode.csrf' = 'some-secret' } + } + } - Get-PodeCsrfSecret | Should Be 'some-secret' + Get-PodeCsrfSecret | Should -Be 'some-secret' } It 'Gets the secret from a cookie' { $PodeContext.Server.Cookies.Csrf.UseCookies = $true Mock Get-PodeCookie { return @{ 'Value' = 'some-secret' } } - Get-PodeCsrfSecret | Should Be 'some-secret' + Get-PodeCsrfSecret | Should -Be 'some-secret' Assert-MockCalled Get-PodeCookie -Times 1 -Scope It } diff --git a/tests/unit/Server.Tests.ps1 b/tests/unit/Server.Tests.ps1 index 80f46599a..911a2a2aa 100644 --- a/tests/unit/Server.Tests.ps1 +++ b/tests/unit/Server.Tests.ps1 @@ -1,35 +1,42 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() -$PodeContext = @{ - Server = $null - Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } - RunspacePools = @{} -} +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + + + $PodeContext = @{ + Server = $null + Metrics = @{ Server = @{ StartTime = [datetime]::UtcNow } } + RunspacePools = @{} + } } Describe 'Start-PodeInternalServer' { - Mock Add-PodePSInbuiltDrives { } - Mock Invoke-PodeScriptBlock { } - Mock New-PodeRunspaceState { } - Mock New-PodeRunspacePools { } - Mock Start-PodeLoggingRunspace { } - Mock Start-PodeTimerRunspace { } - Mock Start-PodeScheduleRunspace { } - Mock Start-PodeGuiRunspace { } - Mock Start-Sleep { } - Mock New-PodeAutoRestartServer { } - Mock Start-PodeSmtpServer { } - Mock Start-PodeTcpServer { } - Mock Start-PodeWebServer { } - Mock Start-PodeServiceServer { } - Mock Import-PodeModulesIntoRunspaceState { } - Mock Import-PodeSnapinsIntoRunspaceState { } - Mock Import-PodeFunctionsIntoRunspaceState { } - Mock Start-PodeCacheHousekeeper { } - Mock Invoke-PodeEvent { } - Mock Write-Verbose { } - Mock Add-PodeScopedVariablesInbuilt { } + BeforeAll { + Mock Add-PodePSInbuiltDrives { } + Mock Invoke-PodeScriptBlock { } + Mock New-PodeRunspaceState { } + Mock New-PodeRunspacePools { } + Mock Start-PodeLoggingRunspace { } + Mock Start-PodeTimerRunspace { } + Mock Start-PodeScheduleRunspace { } + Mock Start-PodeGuiRunspace { } + Mock Start-Sleep { } + Mock New-PodeAutoRestartServer { } + Mock Start-PodeSmtpServer { } + Mock Start-PodeTcpServer { } + Mock Start-PodeWebServer { } + Mock Start-PodeServiceServer { } + Mock Import-PodeModulesIntoRunspaceState { } + Mock Import-PodeSnapinsIntoRunspaceState { } + Mock Import-PodeFunctionsIntoRunspaceState { } + Mock Start-PodeCacheHousekeeper { } + Mock Invoke-PodeEvent { } + Mock Write-Verbose { } + Mock Add-PodeScopedVariablesInbuilt { } + } It 'Calls one-off script logic' { $PodeContext.Server = @{ Types = ([string]::Empty); Logic = {} } @@ -89,15 +96,16 @@ Describe 'Start-PodeInternalServer' { } Describe 'Restart-PodeInternalServer' { - Mock Write-Host { } - Mock Close-PodeRunspaces { } - Mock Remove-PodePSDrives { } - Mock Open-PodeConfiguration { return $null } - Mock Start-PodeInternalServer { } - Mock Write-PodeErrorLog { } - Mock Close-PodeDisposable { } - Mock Invoke-PodeEvent { } - + BeforeAll { + Mock Write-Host { } + Mock Close-PodeRunspaces { } + Mock Remove-PodePSDrives { } + Mock Open-PodeConfiguration { return $null } + Mock Start-PodeInternalServer { } + Mock Write-PodeErrorLog { } + Mock Close-PodeDisposable { } + Mock Invoke-PodeEvent { } + } It 'Resetting the server values' { $PodeContext = @{ Tokens = @{ @@ -229,30 +237,30 @@ Describe 'Restart-PodeInternalServer' { Restart-PodeInternalServer | Out-Null - $PodeContext.Server.Routes['GET'].Count | Should Be 0 - $PodeContext.Server.Logging.Types.Count | Should Be 0 - $PodeContext.Server.Middleware.Count | Should Be 0 - $PodeContext.Server.Endware.Count | Should Be 0 - $PodeContext.Server.Sessions.Count | Should Be 0 - $PodeContext.Server.Authentications.Methods.Count | Should Be 0 - $PodeContext.Server.State.Count | Should Be 0 - $PodeContext.Server.Configuration | Should Be $null + $PodeContext.Server.Routes['GET'].Count | Should -Be 0 + $PodeContext.Server.Logging.Types.Count | Should -Be 0 + $PodeContext.Server.Middleware.Count | Should -Be 0 + $PodeContext.Server.Endware.Count | Should -Be 0 + $PodeContext.Server.Sessions.Count | Should -Be 0 + $PodeContext.Server.Authentications.Methods.Count | Should -Be 0 + $PodeContext.Server.State.Count | Should -Be 0 + $PodeContext.Server.Configuration | Should -Be $null - $PodeContext.Timers.Items.Count | Should Be 0 - $PodeContext.Schedules.Items.Count | Should Be 0 + $PodeContext.Timers.Items.Count | Should -Be 0 + $PodeContext.Schedules.Items.Count | Should -Be 0 - $PodeContext.Server.ViewEngine.Type | Should Be 'html' - $PodeContext.Server.ViewEngine.Extension | Should Be 'html' - $PodeContext.Server.ViewEngine.ScriptBlock | Should Be $null - $PodeContext.Server.ViewEngine.UsingVariables | Should Be $null - $PodeContext.Server.ViewEngine.IsDynamic | Should Be $false + $PodeContext.Server.ViewEngine.Type | Should -Be 'html' + $PodeContext.Server.ViewEngine.Extension | Should -Be 'html' + $PodeContext.Server.ViewEngine.ScriptBlock | Should -Be $null + $PodeContext.Server.ViewEngine.UsingVariables | Should -Be $null + $PodeContext.Server.ViewEngine.IsDynamic | Should -Be $false - $PodeContext.Metrics.Server.RestartCount | Should Be 1 + $PodeContext.Metrics.Server.RestartCount | Should -Be 1 } It 'Catches exception and throws it' { Mock Write-Host { throw 'some error' } Mock Write-PodeErrorLog {} - { Restart-PodeInternalServer } | Should Throw 'some error' + { Restart-PodeInternalServer } | Should -Throw -ExpectedMessage 'some error' } } \ No newline at end of file diff --git a/tests/unit/Serverless.Tests.ps1 b/tests/unit/Serverless.Tests.ps1 index 2cba95f0f..6366d7895 100644 --- a/tests/unit/Serverless.Tests.ps1 +++ b/tests/unit/Serverless.Tests.ps1 @@ -1,27 +1,32 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Start-PodeAzFuncServer' { - function Push-OutputBinding($Name, $Value) { - return @{ Name = $Name; Value = $Value } - } - - Mock Get-PodePublicMiddleware { } - Mock Get-PodeRouteValidateMiddleware { } - Mock Get-PodeBodyMiddleware { } - Mock Get-PodeCookieMiddleware { } - Mock New-Object { return @{} } - Mock Get-PodeHeader { return 'some-value' } - Mock Invoke-PodeScriptBlock { } - Mock Write-Host { } - Mock Invoke-PodeEndware { } - Mock Set-PodeServerHeader { } - Mock Set-PodeResponseStatus { } - Mock Update-PodeServerRequestMetrics { } + BeforeAll { + function Push-OutputBinding($Name, $Value) { + return @{ Name = $Name; Value = $Value } + } + Mock Get-PodePublicMiddleware { } + Mock Get-PodeRouteValidateMiddleware { } + Mock Get-PodeBodyMiddleware { } + Mock Get-PodeCookieMiddleware { } + Mock New-Object { return @{} } + Mock Get-PodeHeader { return 'some-value' } + Mock Invoke-PodeScriptBlock { } + Mock Write-Host { } + Mock Invoke-PodeEndware { } + Mock Set-PodeServerHeader { } + Mock Set-PodeResponseStatus { } + Mock Update-PodeServerRequestMetrics { } + } It 'Throws error for null data' { - { Start-PodeAzFuncServer -Data $null } | Should Throw 'because it is null' + { Start-PodeAzFuncServer -Data $null } | Should -Throw -ExpectedMessage '*because it is null*' } It 'Runs the server, fails middleware with no route' { @@ -31,16 +36,16 @@ Describe 'Start-PodeAzFuncServer' { $result = Start-PodeAzFuncServer -Data @{ Request = @{ Method = 'get' - Query = @{} - Url = 'http://example.com' - }; - sys = @{ + Query = @{} + Url = 'http://example.com' + } + sys = @{ MethodName = 'example' } } - $result.Name | Should Be 'Response' - $result.Value | Should Not Be $null + $result.Name | Should -Be 'Response' + $result.Value | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It @@ -53,16 +58,16 @@ Describe 'Start-PodeAzFuncServer' { $result = Start-PodeAzFuncServer -Data @{ Request = @{ Method = 'get' - Query = @{ 'Static-File' = '.a/path/file.txt' } - Url = 'http://example.com' - }; - sys = @{ + Query = @{ 'Static-File' = '.a/path/file.txt' } + Url = 'http://example.com' + } + sys = @{ MethodName = 'example' } } - $result.Name | Should Be 'Response' - $result.Value | Should Not Be $null + $result.Name | Should -Be 'Response' + $result.Value | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It @@ -75,16 +80,16 @@ Describe 'Start-PodeAzFuncServer' { $result = Start-PodeAzFuncServer -Data @{ Request = @{ Method = 'get' - Query = @{} - Url = 'http://example.com' - }; - sys = @{ + Query = @{} + Url = 'http://example.com' + } + sys = @{ MethodName = 'example' } } - $result.Name | Should Be 'Response' - $result.Value | Should Not Be $null + $result.Name | Should -Be 'Response' + $result.Value | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 2 -Scope It @@ -98,16 +103,16 @@ Describe 'Start-PodeAzFuncServer' { $result = Start-PodeAzFuncServer -Data @{ Request = @{ Method = 'get' - Query = @{} - Url = 'http://example.com' - }; - sys = @{ + Query = @{} + Url = 'http://example.com' + } + sys = @{ MethodName = 'example' } } - $result.Name | Should Be 'Response' - $result.Value | Should Not Be $null + $result.Name | Should -Be 'Response' + $result.Value | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 1 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It @@ -121,15 +126,15 @@ Describe 'Start-PodeAzFuncServer' { $d = @{ Request = @{ Method = 'get' - Query = @{} - Url = 'http://example.com' - }; - sys = @{ + Query = @{} + Url = 'http://example.com' + } + sys = @{ MethodName = 'example' } } - { Start-PodeAzFuncServer -Data $d } | Should Throw 'some error' + { Start-PodeAzFuncServer -Data $d } | Should -Throw -ExpectedMessage 'some error' Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It @@ -137,21 +142,22 @@ Describe 'Start-PodeAzFuncServer' { } Describe 'Start-PodeAwsLambdaServer' { - Mock Get-PodePublicMiddleware { } - Mock Get-PodeRouteValidateMiddleware { } - Mock Get-PodeBodyMiddleware { } - Mock Get-PodeCookieMiddleware { } - Mock Get-PodeHeader { return 'some-value' } - Mock Set-PodeHeader { } - Mock Invoke-PodeScriptBlock { } - Mock Write-Host { } - Mock Invoke-PodeEndware { } - Mock Set-PodeServerHeader { } - Mock Set-PodeResponseStatus { } - Mock Update-PodeServerRequestMetrics { } + BeforeAll { + Mock Get-PodePublicMiddleware { } + Mock Get-PodeRouteValidateMiddleware { } + Mock Get-PodeBodyMiddleware { } + Mock Get-PodeCookieMiddleware { } + Mock Get-PodeHeader { return 'some-value' } + Mock Set-PodeHeader { } + Mock Invoke-PodeScriptBlock { } + Mock Write-Host { } + Mock Invoke-PodeEndware { } + Mock Set-PodeServerHeader { } + Mock Set-PodeResponseStatus { } + Mock Update-PodeServerRequestMetrics { } } It 'Throws error for null data' { - { Start-PodeAwsLambdaServer -Data $null } | Should Throw 'because it is null' + { Start-PodeAwsLambdaServer -Data $null } | Should -Throw -ExpectedMessage '*because it is null*' } It 'Runs the server, fails middleware with no route' { @@ -159,12 +165,12 @@ Describe 'Start-PodeAwsLambdaServer' { $PodeContext = @{ Server = @{ } } $result = Start-PodeAwsLambdaServer -Data @{ - httpMethod = 'get' + httpMethod = 'get' queryStringParameters = @{} - path = '/api/users' + path = '/api/users' } - $result | Should Not Be $null + $result | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It @@ -175,12 +181,12 @@ Describe 'Start-PodeAwsLambdaServer' { $PodeContext = @{ Server = @{ } } $result = Start-PodeAwsLambdaServer -Data @{ - httpMethod = 'get' + httpMethod = 'get' queryStringParameters = @{} - path = '/api/users' + path = '/api/users' } - $result | Should Not Be $null + $result | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 2 -Scope It @@ -192,12 +198,12 @@ Describe 'Start-PodeAwsLambdaServer' { $PodeContext = @{ Server = @{ } } $result = Start-PodeAwsLambdaServer -Data @{ - httpMethod = 'get' + httpMethod = 'get' queryStringParameters = @{} - path = '/api/users' + path = '/api/users' } - $result | Should Not Be $null + $result | Should -Not -Be $null Assert-MockCalled Set-PodeResponseStatus -Times 1 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It @@ -209,12 +215,12 @@ Describe 'Start-PodeAwsLambdaServer' { $PodeContext = @{ Server = @{ } } $d = @{ - httpMethod = 'get' + httpMethod = 'get' queryStringParameters = @{} - path = '/api/users' + path = '/api/users' } - { Start-PodeAwsLambdaServer -Data $d } | Should Throw 'some error' + { Start-PodeAwsLambdaServer -Data $d } | Should -Throw -ExpectedMessage 'some error' Assert-MockCalled Set-PodeResponseStatus -Times 0 -Scope It Assert-MockCalled Invoke-PodeMiddleware -Times 1 -Scope It diff --git a/tests/unit/Sessions.Tests.ps1 b/tests/unit/Sessions.Tests.ps1 index 266a634f5..5b5deb390 100644 --- a/tests/unit/Sessions.Tests.ps1 +++ b/tests/unit/Sessions.Tests.ps1 @@ -1,8 +1,13 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() -$now = [datetime]::UtcNow +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + + $now = [datetime]::UtcNow +} Describe 'Get-PodeSession' { Context 'Invalid parameters supplied' { @@ -15,7 +20,7 @@ Describe 'Get-PodeSession' { } } - { Get-PodeSession } | Should Throw 'because it is an empty string' + { Get-PodeSession } | Should -Throw -ExpectedMessage '*because it is an empty string*' } It 'Throws an empry string value error' { @@ -27,7 +32,7 @@ Describe 'Get-PodeSession' { } } - { Get-PodeSession } | Should Throw 'because it is an empty string' + { Get-PodeSession } | Should -Throw -ExpectedMessage '*because it is an empty string*' } } @@ -47,7 +52,7 @@ Describe 'Get-PodeSession' { } $data = Get-PodeSession - $data | Should Be $null + $data | Should -Be $null } It 'Returns no session details for invalid signed sessionId' { @@ -55,7 +60,7 @@ Describe 'Get-PodeSession' { $WebEvent = @{ Cookies = @{ 'pode.sid' = $cookie - } + } } $PodeContext = @{ @@ -70,7 +75,7 @@ Describe 'Get-PodeSession' { } $data = Get-PodeSession - $data | Should Be $null + $data | Should -Be $null } It 'Returns session details' { @@ -78,7 +83,7 @@ Describe 'Get-PodeSession' { $WebEvent = @{ Cookies = @{ 'pode.sid' = $cookie - } + } } $PodeContext = @{ @@ -93,9 +98,9 @@ Describe 'Get-PodeSession' { } $data = Get-PodeSession - $data | Should Not Be $null - $data.Id | Should Be 'value' - $data.Name | Should Be 'pode.sid' + $data | Should -Not -Be $null + $data.Id | Should -Be 'value' + $data.Name | Should -Be 'pode.sid' } } } @@ -103,7 +108,7 @@ Describe 'Get-PodeSession' { Describe 'Set-PodeSessionDataHash' { Context 'Invalid parameters supplied' { It 'Throws null value error' { - { Set-PodeSessionDataHash } | Should Throw 'No session available' + { Set-PodeSessionDataHash } | Should -Throw -ExpectedMessage '*No session available*' } } @@ -113,13 +118,13 @@ Describe 'Set-PodeSessionDataHash' { Session = @{} } Set-PodeSessionDataHash - $WebEvent.Session.Data | Should Not Be $null + $WebEvent.Session.Data | Should -Not -Be $null $crypto = [System.Security.Cryptography.SHA256]::Create() $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data | ConvertTo-Json -Depth 10 -Compress))) $hash = [System.Convert]::ToBase64String($hash) - $WebEvent.Session.DataHash | Should Be $hash + $WebEvent.Session.DataHash | Should -Be $hash } It 'Sets a hash for data' { @@ -127,20 +132,21 @@ Describe 'Set-PodeSessionDataHash' { Session = @{ 'Data' = @{ 'Counter' = 2; } } } Set-PodeSessionDataHash - $WebEvent.Session.Data | Should Not Be $null + $WebEvent.Session.Data | Should -Not -Be $null $crypto = [System.Security.Cryptography.SHA256]::Create() $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data | ConvertTo-Json -Depth 10 -Compress))) $hash = [System.Convert]::ToBase64String($hash) - $WebEvent.Session.DataHash | Should Be $hash + $WebEvent.Session.DataHash | Should -Be $hash } } } Describe 'New-PodeSession' { - Mock 'Invoke-PodeScriptBlock' { return 'value' } - + BeforeAll { + Mock 'Invoke-PodeScriptBlock' { return 'value' } + } It 'Creates a new session object' { $WebEvent = @{ Session = @{} @@ -161,16 +167,16 @@ Describe 'New-PodeSession' { $WebEvent.Session = New-PodeSession Set-PodeSessionDataHash - $WebEvent.Session | Should Not Be $null - $WebEvent.Session.Id | Should Be 'value' - $WebEvent.Session.Name | Should Be 'pode.sid' - $WebEvent.Session.Data.Count | Should Be 0 + $WebEvent.Session | Should -Not -Be $null + $WebEvent.Session.Id | Should -Be 'value' + $WebEvent.Session.Name | Should -Be 'pode.sid' + $WebEvent.Session.Data.Count | Should -Be 0 $crypto = [System.Security.Cryptography.SHA256]::Create() $hash = $crypto.ComputeHash([System.Text.Encoding]::UTF8.GetBytes(($WebEvent.Session.Data | ConvertTo-Json -Depth 10 -Compress))) $hash = [System.Convert]::ToBase64String($hash) - $WebEvent.Session.DataHash | Should Be $hash + $WebEvent.Session.DataHash | Should -Be $hash } } @@ -180,14 +186,14 @@ Describe 'Test-PodeSessionDataHash' { $WebEvent = @{ Session = @{} } - Test-PodeSessionDataHash | Should Be $false + Test-PodeSessionDataHash | Should -Be $false } It 'Returns false for invalid hash' { $WebEvent = @{ Session = @{ 'DataHash' = 'fake' } } - Test-PodeSessionDataHash | Should Be $false + Test-PodeSessionDataHash | Should -Be $false } It 'Returns true for a valid hash' { @@ -202,7 +208,7 @@ Describe 'Test-PodeSessionDataHash' { $hash = [System.Convert]::ToBase64String($hash) $WebEvent.Session.DataHash = $hash - Test-PodeSessionDataHash | Should Be $true + Test-PodeSessionDataHash | Should -Be $true } } } @@ -210,13 +216,13 @@ Describe 'Test-PodeSessionDataHash' { Describe 'Get-PodeSessionInMemStore' { It 'Returns a valid storage object' { $store = Get-PodeSessionInMemStore - $store | Should Not Be $null + $store | Should -Not -Be $null $members = @(($store | Get-Member).Name) - $members.Contains('Memory' ) | Should Be $true - $members.Contains('Delete' ) | Should Be $true - $members.Contains('Get' ) | Should Be $true - $members.Contains('Set' ) | Should Be $true + $members.Contains('Memory' ) | Should -Be $true + $members.Contains('Delete' ) | Should -Be $true + $members.Contains('Get' ) | Should -Be $true + $members.Contains('Set' ) | Should -Be $true } } @@ -224,8 +230,8 @@ Describe 'Set-PodeSessionInMemClearDown' { It 'Adds a new schedule for clearing down' { $PodeContext = @{ 'Schedules' = @{ Items = @{} } } Set-PodeSessionInMemClearDown - $PodeContext.Schedules.Items.Count | Should Be 1 - $PodeContext.Schedules.Items.Contains('__pode_session_inmem_cleanup__') | Should Be $true + $PodeContext.Schedules.Items.Count | Should -Be 1 + $PodeContext.Schedules.Items.Contains('__pode_session_inmem_cleanup__') | Should -Be $true } } @@ -252,7 +258,7 @@ Describe 'Set-PodeSession' { Describe 'Remove-PodeSession' { It 'Throws an error if sessions are not configured' { Mock Test-PodeSessionsEnabled { return $false } - { Remove-PodeSession } | Should Throw 'Sessions have not been configured' + { Remove-PodeSession } | Should -Throw 'Sessions have not been configured' } It 'Does nothing if there is no session' { @@ -279,13 +285,13 @@ Describe 'Remove-PodeSession' { Describe 'Save-PodeSession' { It 'Throws an error if sessions are not configured' { Mock Test-PodeSessionsEnabled { return $false } - { Save-PodeSession } | Should Throw 'Sessions have not been configured' + { Save-PodeSession } | Should -Throw 'Sessions have not been configured' } It 'Throws error if there is no session' { Mock Test-PodeSessionsEnabled { return $true } $WebEvent = @{} - { Save-PodeSession } | Should Throw 'There is no session available to save' + { Save-PodeSession } | Should -Throw -ExpectedMessage 'There is no session available to save' } It 'Call saves the session' { @@ -294,7 +300,7 @@ Describe 'Save-PodeSession' { $WebEvent = @{ Session = @{ Save = {} - } + } } Save-PodeSession diff --git a/tests/unit/State.Tests.ps1 b/tests/unit/State.Tests.ps1 index 29369ba81..b7feec3b5 100644 --- a/tests/unit/State.Tests.ps1 +++ b/tests/unit/State.Tests.ps1 @@ -1,56 +1,61 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() -$PodeContext = @{ 'Server' = $null; } +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + + $PodeContext = @{ 'Server' = $null; } +} Describe 'Set-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Set-PodeState -Name 'test' } | Should Throw 'Pode has not been initialised' + { Set-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' } It 'Sets and returns an object' { $PodeContext.Server = @{ 'State' = @{} } $result = Set-PodeState -Name 'test' -Value 7 - $result | Should Be 7 - $PodeContext.Server.State['test'].Value | Should Be 7 - $PodeContext.Server.State['test'].Scope | Should Be @() + $result | Should -Be 7 + $PodeContext.Server.State['test'].Value | Should -Be 7 + $PodeContext.Server.State['test'].Scope | Should -Be @() } } Describe 'Get-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Get-PodeState -Name 'test' } | Should Throw 'Pode has not been initialised' + { Get-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' } It 'Gets an object from the state' { $PodeContext.Server = @{ 'State' = @{} } Set-PodeState -Name 'test' -Value 8 - Get-PodeState -Name 'test' | Should Be 8 + Get-PodeState -Name 'test' | Should -Be 8 } } Describe 'Remove-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Remove-PodeState -Name 'test' } | Should Throw 'Pode has not been initialised' + { Remove-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' } It 'Removes an object from the state' { $PodeContext.Server = @{ 'State' = @{} } Set-PodeState -Name 'test' -Value 8 - Remove-PodeState -Name 'test' | Should Be 8 - $PodeContext.Server.State['test'] | Should Be $null + Remove-PodeState -Name 'test' | Should -Be 8 + $PodeContext.Server.State['test'] | Should -Be $null } } Describe 'Save-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Save-PodeState -Path 'some/path' } | Should Throw 'Pode has not been initialised' + { Save-PodeState -Path 'some/path' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' } It 'Saves the state to file' { @@ -90,7 +95,7 @@ Describe 'Save-PodeState' { Describe 'Restore-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Restore-PodeState -Path 'some/path' } | Should Throw 'Pode has not been initialised' + { Restore-PodeState -Path 'some/path' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' } It 'Restores the state from file' { @@ -100,25 +105,25 @@ Describe 'Restore-PodeState' { $PodeContext.Server = @{ 'State' = @{} } Restore-PodeState -Path './state.json' - Get-PodeState -Name 'Name' | Should Be 'Morty' + Get-PodeState -Name 'Name' | Should -Be 'Morty' } } Describe 'Test-PodeState' { It 'Throws error when not initialised' { $PodeContext.Server = @{ 'State' = $null } - { Test-PodeState -Name 'test' } | Should Throw 'Pode has not been initialised' + { Test-PodeState -Name 'test' } | Should -Throw -ExpectedMessage 'Pode has not been initialised' } It 'Returns true for an object being in the state' { $PodeContext.Server = @{ 'State' = @{} } Set-PodeState -Name 'test' -Value 8 - Test-PodeState -Name 'test' | Should Be $true + Test-PodeState -Name 'test' | Should -Be $true } It 'Returns false for an object not being in the state' { $PodeContext.Server = @{ 'State' = @{} } Set-PodeState -Name 'test' -Value 8 - Test-PodeState -Name 'tests' | Should Be $false + Test-PodeState -Name 'tests' | Should -Be $false } } \ No newline at end of file diff --git a/tests/unit/Timers.Tests.ps1 b/tests/unit/Timers.Tests.ps1 index 418a2a806..c8b6e6076 100644 --- a/tests/unit/Timers.Tests.ps1 +++ b/tests/unit/Timers.Tests.ps1 @@ -1,30 +1,37 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param() + +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } + + $PodeContext = @{ 'Server' = $null; } +} Describe 'Find-PodeTimer' { Context 'Invalid parameters supplied' { It 'Throw null name parameter error' { - { Find-PodeTimer -Name $null } | Should Throw 'The argument is null or empty' + { Find-PodeTimer -Name $null } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } It 'Throw empty name parameter error' { - { Find-PodeTimer -Name ([string]::Empty) } | Should Throw 'The argument is null or empty' + { Find-PodeTimer -Name ([string]::Empty) } | Should -Throw -ExpectedMessage '*The argument is null or empty*' } } Context 'Valid values supplied' { It 'Returns null as the timer does not exist' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - Find-PodeTimer -Name 'test' | Should Be $null + Find-PodeTimer -Name 'test' | Should -Be $null } It 'Returns timer for name' { $PodeContext = @{ 'Timers' = @{ Items = @{ 'test' = @{ 'Name' = 'test'; }; } }; } $result = (Find-PodeTimer -Name 'test') - $result | Should BeOfType System.Collections.Hashtable - $result.Name | Should Be 'test' + $result | Should -BeOfType System.Collections.Hashtable + $result.Name | Should -Be 'test' } } } @@ -32,27 +39,27 @@ Describe 'Find-PodeTimer' { Describe 'Add-PodeTimer' { It 'Throws error because timer already exists' { $PodeContext = @{ 'Timers' = @{ Items = @{ 'test' = $null }; } } - { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} } | Should Throw 'already defined' + { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*already defined*' } It 'Throws error because interval is 0' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval 0 -ScriptBlock {} } | Should Throw 'interval must be greater than 0' + { Add-PodeTimer -Name 'test' -Interval 0 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*interval must be greater than 0*' } It 'Throws error because interval is less than 0' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval -1 -ScriptBlock {} } | Should Throw 'interval must be greater than 0' + { Add-PodeTimer -Name 'test' -Interval -1 -ScriptBlock {} } | Should -Throw -ExpectedMessage '*interval must be greater than 0*' } It 'Throws error because limit is negative' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Limit -1 } | Should Throw 'negative limit' + { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Limit -1 } | Should -Throw -ExpectedMessage '*negative limit*' } It 'Throws error because skip is negative' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } - { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Skip -1 } | Should Throw 'negative skip' + { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock {} -Skip -1 } | Should -Throw -ExpectedMessage '*negative skip*' } It 'Adds new timer to session with no limit' { @@ -60,15 +67,15 @@ Describe 'Add-PodeTimer' { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 $timer = $PodeContext.Timers.Items['test'] - $timer | Should Not Be $null - $timer.Name | Should Be 'test' - $timer.Interval | Should Be 1 - $timer.Limit | Should Be 0 - $timer.Count | Should Be 0 - $timer.Skip | Should Be 1 - $timer.NextTriggerTime | Should BeOfType System.DateTime - $timer.Script | Should Not Be $null - $timer.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $timer | Should -Not -Be $null + $timer.Name | Should -Be 'test' + $timer.Interval | Should -Be 1 + $timer.Limit | Should -Be 0 + $timer.Count | Should -Be 0 + $timer.Skip | Should -Be 1 + $timer.NextTriggerTime | Should -BeOfType System.DateTime + $timer.Script | Should -Not -Be $null + $timer.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } It 'Adds new timer to session with limit' { @@ -76,15 +83,15 @@ Describe 'Add-PodeTimer' { Add-PodeTimer -Name 'test' -Interval 3 -ScriptBlock { Write-Host 'hello' } -Limit 2 -Skip 1 $timer = $PodeContext.Timers.Items['test'] - $timer | Should Not Be $null - $timer.Name | Should Be 'test' - $timer.Interval | Should Be 3 - $timer.Limit | Should Be 2 - $timer.Count | Should Be 0 - $timer.Skip | Should Be 1 - $timer.NextTriggerTime | Should BeOfType System.DateTime - $timer.Script | Should Not Be $null - $timer.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $timer | Should -Not -Be $null + $timer.Name | Should -Be 'test' + $timer.Interval | Should -Be 3 + $timer.Limit | Should -Be 2 + $timer.Count | Should -Be 0 + $timer.Skip | Should -Be 1 + $timer.NextTriggerTime | Should -BeOfType System.DateTime + $timer.Script | Should -Not -Be $null + $timer.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() } } @@ -92,7 +99,7 @@ Describe 'Get-PodeTimer' { It 'Returns no timers' { $PodeContext = @{ Timers = @{ Items = @{} } } $timers = Get-PodeTimer - $timers.Length | Should Be 0 + $timers.Length | Should -Be 0 } It 'Returns 1 timer by name' { @@ -100,12 +107,12 @@ Describe 'Get-PodeTimer' { Add-PodeTimer -Name 'test1' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 $timers = Get-PodeTimer - $timers.Length | Should Be 1 + $timers.Length | Should -Be 1 - $timers.Name | Should Be 'test1' - $timers.Interval | Should Be 1 - $timers.Skip | Should Be 1 - $timers.Limit | Should Be 0 + $timers.Name | Should -Be 'test1' + $timers.Interval | Should -Be 1 + $timers.Skip | Should -Be 1 + $timers.Limit | Should -Be 0 } It 'Returns 2 timers by name' { @@ -116,7 +123,7 @@ Describe 'Get-PodeTimer' { Add-PodeTimer -Name 'test3' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 $timers = Get-PodeTimer -Name test1, test2 - $timers.Length | Should Be 2 + $timers.Length | Should -Be 2 } It 'Returns all timers' { @@ -127,7 +134,7 @@ Describe 'Get-PodeTimer' { Add-PodeTimer -Name 'test3' -Interval 1 -ScriptBlock { Write-Host 'hello' } -Limit 0 -Skip 1 $timers = Get-PodeTimer - $timers.Length | Should Be 3 + $timers.Length | Should -Be 3 } } @@ -137,13 +144,13 @@ Describe 'Remove-PodeTimer' { Add-PodeTimer -Name 'test' -Interval 1 -ScriptBlock { Write-Host 'hello' } $timer = $PodeContext.Timers.Items['test'] - $timer.Name | Should Be 'test' - $timer.Script.ToString() | Should Be ({ Write-Host 'hello' }).ToString() + $timer.Name | Should -Be 'test' + $timer.Script.ToString() | Should -Be ({ Write-Host 'hello' }).ToString() Remove-PodeTimer -Name 'test' $timer = $PodeContext.Timers.Items['test'] - $timer | Should Be $null + $timer | Should -Be $null } } @@ -153,11 +160,11 @@ Describe 'Clear-PodeTimers' { Add-PodeTimer -Name 'test1' -Interval 1 -ScriptBlock { Write-Host 'hello1' } Add-PodeTimer -Name 'test2' -Interval 1 -ScriptBlock { Write-Host 'hello2' } - $PodeContext.Timers.Items.Count | Should Be 2 + $PodeContext.Timers.Items.Count | Should -Be 2 Clear-PodeTimers - $PodeContext.Timers.Items.Count | Should Be 0 + $PodeContext.Timers.Items.Count | Should -Be 0 } } @@ -165,22 +172,22 @@ Describe 'Edit-PodeTimer' { It 'Adds a new timer, then edits the interval' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } Add-PodeTimer -Name 'test1' -Interval 1 -ScriptBlock { Write-Host 'hello1' } - $PodeContext.Timers.Items['test1'].Interval | Should Be 1 - $PodeContext.Timers.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() + $PodeContext.Timers.Items['test1'].Interval | Should -Be 1 + $PodeContext.Timers.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() Edit-PodeTimer -Name 'test1' -Interval 3 - $PodeContext.Timers.Items['test1'].Interval | Should Be 3 - $PodeContext.Timers.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() + $PodeContext.Timers.Items['test1'].Interval | Should -Be 3 + $PodeContext.Timers.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() } It 'Adds a new timer, then edits the script' { $PodeContext = @{ 'Timers' = @{ Items = @{} }; } Add-PodeTimer -Name 'test1' -Interval 1 -ScriptBlock { Write-Host 'hello1' } - $PodeContext.Timers.Items['test1'].Interval | Should Be 1 - $PodeContext.Timers.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello1' }).ToString() + $PodeContext.Timers.Items['test1'].Interval | Should -Be 1 + $PodeContext.Timers.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello1' }).ToString() Edit-PodeTimer -Name 'test1' -ScriptBlock { Write-Host 'hello2' } - $PodeContext.Timers.Items['test1'].Interval | Should Be 1 - $PodeContext.Timers.Items['test1'].Script.ToString() | Should Be ({ Write-Host 'hello2' }).ToString() + $PodeContext.Timers.Items['test1'].Interval | Should -Be 1 + $PodeContext.Timers.Items['test1'].Script.ToString() | Should -Be ({ Write-Host 'hello2' }).ToString() } } \ No newline at end of file diff --git a/tests/unit/_.Tests.ps1 b/tests/unit/_.Tests.ps1 index 259843a55..59ce7d5e6 100644 --- a/tests/unit/_.Tests.ps1 +++ b/tests/unit/_.Tests.ps1 @@ -1,10 +1,13 @@ -$path = $MyInvocation.MyCommand.Path -$src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' -Import-Module "$($src)/Pode.psm1" -Force +BeforeAll { + $path = $PSCommandPath + $src = (Split-Path -Parent -Path $path) -ireplace '[\\/]tests[\\/]unit', '/src/' + Get-ChildItem "$($src)/*.ps1" -Recurse | Resolve-Path | ForEach-Object { . $_ } +} Describe 'Exported Functions' { It 'Have Parameter Descriptions' { - $funcs = (Get-Module Pode).ExportedFunctions.Values.Name + $psDataFile = Import-PowerShellDataFile "$src/Pode.psd1" + $funcs = $psDataFile.FunctionsToExport $found = @() foreach ($func in $funcs) { @@ -16,6 +19,6 @@ Describe 'Exported Functions' { } } - $found | Should Be @() + $found | Should -Be @() } } \ No newline at end of file From 16d042a19a41ae144b2f9070eda89d323ba48ad1 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 12:56:34 -0700 Subject: [PATCH 33/84] Update GitHub workflow #1263 --- .github/workflows/PSScriptAnalyzer.yml | 49 ++++++++++++++++++++++++ .github/workflows/ci-coverage.yml | 9 ++++- .github/workflows/ci-powershell.yml | 43 +++++++++++++++++++++ .github/workflows/ci-pwsh7_3.yml | 53 ++++++++++++++++++++++++++ .github/workflows/ci-pwsh7_4.yml | 53 ++++++++++++++++++++++++++ .github/workflows/ci-pwsh7_5.yml | 53 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 15 +++++--- 7 files changed, 268 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/PSScriptAnalyzer.yml create mode 100644 .github/workflows/ci-powershell.yml create mode 100644 .github/workflows/ci-pwsh7_3.yml create mode 100644 .github/workflows/ci-pwsh7_4.yml create mode 100644 .github/workflows/ci-pwsh7_5.yml diff --git a/.github/workflows/PSScriptAnalyzer.yml b/.github/workflows/PSScriptAnalyzer.yml new file mode 100644 index 000000000..9372857fd --- /dev/null +++ b/.github/workflows/PSScriptAnalyzer.yml @@ -0,0 +1,49 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. +# +# https://github.com/microsoft/action-psscriptanalyzer +# For more information on PSScriptAnalyzer in general, see +# https://github.com/PowerShell/PSScriptAnalyzer + +name: PSScriptAnalyzer + +on: + push: + branches: [ "develop" ] + pull_request: + branches: [ "develop" ] + schedule: + - cron: '20 16 * * 6' + +permissions: + contents: read + +jobs: + build: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + name: PSScriptAnalyzer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run PSScriptAnalyzer + uses: microsoft/psscriptanalyzer-action@6b2948b1944407914a58661c49941824d149734f + with: + # Check https://github.com/microsoft/action-psscriptanalyzer for more info about the options. + # The below set up runs PSScriptAnalyzer to your entire repository and runs some basic security rules. + path: .\ + recurse: true + # Include your own basic security rules. Removing this option will run all the rules + includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' + output: results.sarif + + # Upload the SARIF file generated in the previous step + - name: Upload SARIF results file + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: results.sarif diff --git a/.github/workflows/ci-coverage.yml b/.github/workflows/ci-coverage.yml index ec505f8cc..1a8de05a2 100644 --- a/.github/workflows/ci-coverage.yml +++ b/.github/workflows/ci-coverage.yml @@ -15,13 +15,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 + + - name: Check PowerShell version + shell: pwsh + run: | + $PSVersionTable.PSVersion - name: Install Invoke-Build shell: pwsh run: | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Install-Module -Name InvokeBuild -RequiredVersion '5.5.1' -Force + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests shell: pwsh diff --git a/.github/workflows/ci-powershell.yml b/.github/workflows/ci-powershell.yml new file mode 100644 index 000000000..0287d587e --- /dev/null +++ b/.github/workflows/ci-powershell.yml @@ -0,0 +1,43 @@ +name: Pode CI - Powershell Desktop + +on: + push: + branches: + - '*' + - '!gh-pages' + pull_request: + branches: + - '*' + +jobs: + build: + + runs-on: windows-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Check PowerShell version + shell: powershell + run: | + $PSVersionTable.PSVersion + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Invoke-Build + shell: powershell + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force + + - name: Run Pester Tests + shell: powershell + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build Test \ No newline at end of file diff --git a/.github/workflows/ci-pwsh7_3.yml b/.github/workflows/ci-pwsh7_3.yml new file mode 100644 index 000000000..8cb94de60 --- /dev/null +++ b/.github/workflows/ci-pwsh7_3.yml @@ -0,0 +1,53 @@ +name: Pode CI - pwsh 7.3 on windows-latest + +on: + push: + branches: + - '*' + - '!gh-pages' + pull_request: + branches: + - '*' + +jobs: + build: + + runs-on: windows-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Invoke-Build + shell: pwsh + run: | + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force + + # For Windows + - name: Download and extract PowerShell 7.3 + run: | + Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.3.11/PowerShell-7.3.11-win-x64.zip" -OutFile "PowerShell-7.3.x-win-x64.zip" + Expand-Archive -LiteralPath "PowerShell-7.3.x-win-x64.zip" -DestinationPath "C:\PowerShell-7.3" + + - name: Check PowerShell version + run: | + C:\PowerShell-7.3\pwsh.exe -version + + - name: Run Pester Tests + run: | + C:\PowerShell-7.3\pwsh.exe -Command { + Invoke-Build Test + } + + - name: Test docker builds + run: | + C:\PowerShell-7.3\pwsh.exe -Command { + Invoke-Build DockerPack -Version '0.0.0' + } diff --git a/.github/workflows/ci-pwsh7_4.yml b/.github/workflows/ci-pwsh7_4.yml new file mode 100644 index 000000000..80d0c1b4a --- /dev/null +++ b/.github/workflows/ci-pwsh7_4.yml @@ -0,0 +1,53 @@ +name: Pode CI - pwsh 7.4 on windows-latest + +on: + push: + branches: + - '*' + - '!gh-pages' + pull_request: + branches: + - '*' + +jobs: + build: + + runs-on: windows-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Invoke-Build + shell: pwsh + run: | + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force + + # For Windows + - name: Download and extract PowerShell 7.4 + run: | + Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.4.1/PowerShell-7.4.1-win-x64.zip" -OutFile "PowerShell-7.4.x-win-x64.zip" + Expand-Archive -LiteralPath "PowerShell-7.4.x-win-x64.zip" -DestinationPath "C:\PowerShell-7.4" + + - name: Check PowerShell version + run: | + C:\PowerShell-7.4\pwsh.exe -version + + - name: Run Pester Tests + run: | + C:\PowerShell-7.4\pwsh.exe -Command { + Invoke-Build Test + } + + - name: Test docker builds + run: | + C:\PowerShell-7.4\pwsh.exe -Command { + Invoke-Build DockerPack -Version '0.0.0' + } diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml new file mode 100644 index 000000000..63757c133 --- /dev/null +++ b/.github/workflows/ci-pwsh7_5.yml @@ -0,0 +1,53 @@ +name: Pode CI - pwsh 7.5 on windows-latest + +on: + push: + branches: + - '*' + - '!gh-pages' + pull_request: + branches: + - '*' + +jobs: + build: + + runs-on: windows-latest + + strategy: + fail-fast: false + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Invoke-Build + shell: pwsh + run: | + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force + + # For Windows + - name: Download and extract PowerShell 7.5 + run: | + Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.5.0-preview.2/PowerShell-7.5.0-preview.2-win-x64.zip" -OutFile "PowerShell-7.5.x-win-x64.zip" + Expand-Archive -LiteralPath "PowerShell-7.5.x-win-x64.zip" -DestinationPath "C:\PowerShell-7.5" + + - name: Check PowerShell version + run: | + C:\PowerShell-7.5\pwsh.exe -version + + - name: Run Pester Tests + run: | + C:\PowerShell-7.5\pwsh.exe -Command { + Invoke-Build Test + } + + - name: Test docker builds + run: | + C:\PowerShell-7.5\pwsh.exe -Command { + Invoke-Build DockerPack -Version '0.0.0' + } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff02d022d..35ac90284 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Pode CI +name: Pode CI - pwsh on: push: @@ -20,18 +20,23 @@ jobs: os: [ubuntu-latest, windows-latest, macOS-latest] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Check PowerShell version + shell: pwsh + run: | + $PSVersionTable.PSVersion - name: Setup .NET - uses: actions/setup-dotnet@v1.9.0 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install Invoke-Build shell: pwsh run: | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 - Install-Module -Name InvokeBuild -RequiredVersion '5.5.1' -Force + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests shell: pwsh From 9f53cb265abe5c6f93e7e7e8914ee501eaaeec61 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 13:26:46 -0700 Subject: [PATCH 34/84] Implement Enhancing Flexibility with Customizable Default Folder Paths in Pode #1243 Set-PodeDefaultFolder: Allows users to programmatically set the path for a default folder during runtime. Parameters: -Type: Specifies the folder type (Views, Public, or Errors). -Path: Defines the new file system path for the folder. Get-PodeDefaultFolder: Enables querying the current path settings for the default folders. --- docs/Tutorials/Configuration.md | 29 +++--- .../Routes/Utilities/StaticContent.md | 2 +- src/Pode.psd1 | 2 + src/Private/Context.ps1 | 19 ++++ src/Private/Helpers.ps1 | 34 +++++-- src/Public/Core.ps1 | 90 ++++++++++++++++++- 6 files changed, 154 insertions(+), 22 deletions(-) diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index 7e91cfa0b..d37c13df3 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -68,17 +68,18 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: } ``` -| Path | Description | Docs | -| ---- | ----------- | ---- | -| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | -| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | -| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | -| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) | -| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | -| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | -| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | -| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | -| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | -| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | -| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | -| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | +| Path | Description | Docs | +| --------------------- | ----------------------------------------------------------------------- | ------------------------------------------ | +| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | +| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | +| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | +| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) | +| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | +| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | +| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | +| Server.DefaultFolders | Define custom default folders | [link](../Routes/Utilities/StaticContent) | +| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | +| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | +| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | +| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | +| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | diff --git a/docs/Tutorials/Routes/Utilities/StaticContent.md b/docs/Tutorials/Routes/Utilities/StaticContent.md index 44da45d17..1aafda415 100644 --- a/docs/Tutorials/Routes/Utilities/StaticContent.md +++ b/docs/Tutorials/Routes/Utilities/StaticContent.md @@ -6,7 +6,7 @@ Caching is supported on static content. ## Public Directory -You can place static files within the `/public` directory, at the root of your server. If a request is made for a file, then Pode will automatically check the public directory first, and if found will return the back. +You can place static files within the `/public` directory at the root of your server, which serves as the default location for static content. However, if you need to relocate this directory, you can do so programmatically using the `Set-PodeStaticFolder` function within your server script, or specify a different location in the `server.psd1` configuration file under the `Server.DefaultFolders` property. When a request is made for a file, Pode will automatically check this designated static directory first, and if the file is found, it will be returned to the requester. For example, if you have a `logic.js` at `/public/scripts/logic.js`. The the following request would return the file's content: diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 72fef9247..048df39cd 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -136,6 +136,8 @@ 'Test-PodeIsHosted', 'New-PodeCron', 'Test-PodeInRunspace', + 'Set-PodeDefaultFolder', + 'Get-PodeDefaultFolder', # routes 'Add-PodeRoute', diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 7643e0850..88bc711cb 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -178,6 +178,13 @@ function New-PodeContext { BodySize = 100MB } + # default Folders + $ctx.Server.DefaultFolders = @{ + 'Views' = 'views' + 'Public' = 'public' + 'Errors' = 'errors' + } + # check if there is any global configuration $ctx.Server.Configuration = Open-PodeConfiguration -ServerRoot $ServerRoot -Context $ctx @@ -819,6 +826,18 @@ function Set-PodeServerConfiguration { if ([long]$Configuration.Request.BodySize -gt 0) { $Context.Server.Request.BodySize = [long]$Configuration.Request.BodySize } + + if ($Configuration.DefaultFolders) { + if ($Configuration.DefaultFolders.Public) { + $Context.Server.DefaultFolders.Public = $Configuration.DefaultFolders.Public + } + if ($Configuration.DefaultFolders.View) { + $Context.Server.DefaultFolders.View = $Configuration.DefaultFolders.View + } + if ($Configuration.DefaultFolders.Errors) { + $Context.Server.DefaultFolders.Errors = $Configuration.DefaultFolders.Errors + } + } } function Set-PodeWebConfiguration { diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index ecc38dc86..18b3a65a3 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -894,23 +894,45 @@ function Import-PodeModules { } } +<# +.SYNOPSIS +Creates and registers inbuilt PowerShell drives for the Pode server's default folders. + +.DESCRIPTION +This function sets up inbuilt PowerShell drives for the Pode web server's default directories: views, public content, and error pages. For each of these directories, if the physical path exists on the server, a new PowerShell drive is created and mapped to this path. These drives provide an easy and consistent way to access server resources like views, static files, and custom error pages within the Pode application. + +The function leverages `$PodeContext` to access the server's configuration and to determine the paths for these default folders. If a folder's path exists, the function uses `New-PodePSDrive` to create a PowerShell drive for it and stores this drive in the server's `InbuiltDrives` dictionary, keyed by the folder type. + +.PARAMETER None + +.EXAMPLE +Add-PodePSInbuiltDrives + +This example is typically called within the Pode server setup script or internally by the Pode framework to initialize the PowerShell drives for the server's default folders. + +.NOTES +- The function is designed to be used within the Pode framework and relies on the global `$PodeContext` variable for configuration. +- It specifically checks for the existence of paths for views, public content, and errors before attempting to create drives for them. +- This is an internal function and may change in future releases of Pode. +#> function Add-PodePSInbuiltDrives { + # create drive for views, if path exists - $path = (Join-PodeServerRoot 'views') + $path = (Join-PodeServerRoot -Folder $PodeContext.Server.DefaultFolders.Views) if (Test-Path $path) { - $PodeContext.Server.InbuiltDrives['views'] = (New-PodePSDrive -Path $path) + $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Views] = (New-PodePSDrive -Path $path) } # create drive for public content, if path exists - $path = (Join-PodeServerRoot 'public') + $path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Public) if (Test-Path $path) { - $PodeContext.Server.InbuiltDrives['public'] = (New-PodePSDrive -Path $path) + $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Public] = (New-PodePSDrive -Path $path) } # create drive for errors, if path exists - $path = (Join-PodeServerRoot 'errors') + $path = (Join-PodeServerRoot $PodeContext.Server.DefaultFolders.Errors) if (Test-Path $path) { - $PodeContext.Server.InbuiltDrives['errors'] = (New-PodePSDrive -Path $path) + $PodeContext.Server.InbuiltDrives[$PodeContext.Server.DefaultFolders.Errors] = (New-PodePSDrive -Path $path) } } diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index afe1aefc8..5ab27fe3b 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -1236,4 +1236,92 @@ function Get-PodeEndpoint { # return return $endpoints -} \ No newline at end of file +} + +<# +.SYNOPSIS +Sets the path for a specified default folder type in the Pode server context. + +.DESCRIPTION +This function configures the path for one of the Pode server's default folder types: Views, Public, or Errors. +It updates the server's configuration to reflect the new path for the specified folder type. +The function first checks if the provided path exists and is a directory; +if so, it updates the `Server.DefaultFolders` dictionary with the new path. +If the path does not exist or is not a directory, the function throws an error. + +The purpose of this function is to allow dynamic configuration of the server's folder paths, which can be useful during server setup or when altering the server's directory structure at runtime. + +.PARAMETER Type +The type of the default folder to set the path for. Must be one of 'Views', 'Public', or 'Errors'. +This parameter determines which default folder's path is being set. + +.PARAMETER Path +The new file system path for the specified default folder type. This path must exist and be a directory; otherwise, an exception is thrown. + +.EXAMPLE +Set-PodeDefaultFolder -Type 'Views' -Path 'C:\Pode\Views' + +This example sets the path for the server's default 'Views' folder to 'C:\Pode\Views', assuming this path exists and is a directory. + +.EXAMPLE +Set-PodeDefaultFolder -Type 'Public' -Path 'C:\Pode\Public' + +This example sets the path for the server's default 'Public' folder to 'C:\Pode\Public'. + +#> +function Set-PodeDefaultFolder { + + [CmdletBinding()] + param ( + [Parameter()] + [ValidateSet('Views', 'Public', 'Errors')] + [string] + $Type, + + [Parameter()] + [string] + $Path + ) + if (Test-Path -Path $Path -PathType Container) { + $PodeContext.Server.DefaultFolders[$Type] = $Path + } + else { + throw "Folder $Path doesn't exist" + } +} + +<# +.SYNOPSIS +Retrieves the path of a specified default folder type from the Pode server context. + +.DESCRIPTION +This function returns the path for one of the Pode server's default folder types: Views, Public, or Errors. It accesses the server's configuration stored in the `$PodeContext` variable and retrieves the path for the specified folder type from the `DefaultFolders` dictionary. This function is useful for scripts or modules that need to dynamically access server resources based on the server's current configuration. + +.PARAMETER Type +The type of the default folder for which to retrieve the path. The valid options are 'Views', 'Public', or 'Errors'. This parameter determines which folder's path will be returned by the function. + +.EXAMPLE +$path = Get-PodeDefaultFolder -Type 'Views' + +This example retrieves the current path configured for the server's 'Views' folder and stores it in the `$path` variable. + +.EXAMPLE +$path = Get-PodeDefaultFolder -Type 'Public' + +This example retrieves the current path configured for the server's 'Public' folder. + +.OUTPUTS +String. The file system path of the specified default folder. +#> +function Get-PodeDefaultFolder { + [CmdletBinding()] + [OutputType([string])] + param ( + [Parameter()] + [ValidateSet('Views', 'Public', 'Errors')] + [string] + $Type + ) + + return $PodeContext.Server.DefaultFolders[$Type] +} From bc3bd8ea4dceeb8b0597598e8b6d6e6f9805797b Mon Sep 17 00:00:00 2001 From: Matthew Kelly Date: Mon, 25 Mar 2024 20:40:12 +0000 Subject: [PATCH 35/84] #1251: add DualMode docs --- docs/Tutorials/Endpoints/Basics.md | 98 ++++++++++++++++++++---------- src/Private/Helpers.ps1 | 4 +- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/docs/Tutorials/Endpoints/Basics.md b/docs/Tutorials/Endpoints/Basics.md index 9bd89a81d..f4dbd93f8 100644 --- a/docs/Tutorials/Endpoints/Basics.md +++ b/docs/Tutorials/Endpoints/Basics.md @@ -1,10 +1,13 @@ # Basics -Endpoints in Pode are used to bind your server to specific IPs, Hostnames and ports, over specific protocols (such as HTTP or HTTPS). Endpoints can have unique names, so you can bind Routes to certain endpoints only. +Endpoints in Pode are used to bind your server to specific IPs, Hostnames, and ports over specific protocols (such as HTTP or HTTPS). Endpoints can have unique names, so you can bind Routes to certain endpoints only. + +!!! tip + Endpoints support both IPv4 and IPv6 addresses. ## Usage -To add new endpoints to your server, you can use [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint). A quick and simple example is the following, which will bind your server to `http://localhost:8080`: +To add new endpoints to your server you use [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint). A quick and simple example is the following, which will bind your server to `http://localhost:8080`: ```powershell Start-PodeServer { @@ -12,10 +15,41 @@ Start-PodeServer { } ``` -The `-Address` can be local or private IP address. The `-Port` is any valid port number, and the `-Protocol` defines which protocol the endpoint will use: HTTP, HTTPS, SMTP, TCP, WS and WSS. +The `-Address` can be a local or a private IP address. The `-Port` is any valid port number, and the `-Protocol` defines which protocol the endpoint will use: HTTP, HTTPS, SMTP, TCP, WS, or WSS. You can also supply an optional unique `-Name` to your endpoint. This name will allow you to bind routes to certain endpoints; so if you have endpoint A and B, and you bind some route to endpoint A, then it won't be accessible over endpoint B. +```powershell +Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name EndpointA +Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name EndpointB + +Add-PodeRoute -Method Get -Path '/page-a' -EndpointName EndpointA -ScriptBlock { + # logic +} + +Add-PodeRoute -Method Get -Path '/page-b' -EndpointName EndpointB -ScriptBlock { + # logic +} +``` + +## Dual Mode + +When you create an Endpoint in Pode, it will listen on either just the IPv4 or IPv6 address that you supplied - for localhost, this will be `127.0.0.1`, and for "*", this will be `0.0.0.0`, unless the IPv6 equivalents of `::1` or `::` were directly supplied. + +This means if you have the following endpoint to listen on "everything", then it will only really be everything for just IPv4: + +```powershell +Add-PodeEndpoint -Address * -Port 8080 -Protocol Http +``` + +However, you can pass the `-DualMode` switch on [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint) and this will tell Pode to listen on both IPv4 and IPv6 instead: + +```powershell +Add-PodeEndpoint -Address * -Port 8080 -Protocol Http -DualMode +``` + +This will work for any IPv4 address, including localhost, if the underlying OS supports IPv6. For IPv6 addresses, only those that can be converted back to an equivalent IPv4 address will work - it will still listen on the supplied IPv6 address, there just won't be an IPv4 that Pode could also listen on. + ## Hostnames You can specify a `-Hostname` for an endpoint, in doing so you can only access routes via the specified hostname. Using a hostname will allow you to have multiple endpoints all using the same IP/Port, but with different hostnames. @@ -32,7 +66,7 @@ To bind a hostname to a specific IP you can use `-Address`: Add-PodeEndpoint -Address 127.0.0.2 -Hostname example.pode.com -Port 8080 -Protocol Http ``` -or, lookup the hostnames IP from host file or DNS: +or, lookup the hostname's IP from the host file or DNS: ```powershell Add-PodeEndpoint -Hostname example.pode.com -Port 8080 -Protocol Http -LookupHostname @@ -49,16 +83,16 @@ Add-PodeEndpoint -Address 127.0.0.3 -Hostname two.pode.com -Port 8080 -Protocol If you add an HTTPS or WSS endpoint, then you'll be required to also supply certificate details. To configure a certificate you can use one of the following parameters: -| Name | Description | -| ---- | ----------- | -| Certificate | The path to a `.pfx` or `.cer` certificate | -| CertificatePassword | The password for the above `.pfx` certificate | -| CertificateThumbprint | The thumbprint of a certificate to find (Windows only) | -| CertificateName | The subject name of a certificate to find (Windows only) | -| CertificateStoreName | The name of the certificate store (Default: My) (Windows only) | -| CertificateStoreLocation | The location of the certificate store (Default: CurrentUser) (Windows only) | -| X509Certificate | A raw X509Certificate object | -| SelfSigned | If supplied, Pode will automatically generate a self-signed certificate as an X509Certificate object | +| Name | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------------- | +| Certificate | The path to a `.pfx` or `.cer` certificate | +| CertificatePassword | The password for the above `.pfx` certificate | +| CertificateThumbprint | The thumbprint of a certificate to find (Windows only) | +| CertificateName | The subject name of a certificate to find (Windows only) | +| CertificateStoreName | The name of the certificate store (Default: My) (Windows only) | +| CertificateStoreLocation | The location of the certificate store (Default: CurrentUser) (Windows only) | +| X509Certificate | A raw X509Certificate object | +| SelfSigned | If supplied, Pode will automatically generate a self-signed certificate as an X509Certificate object | The below example will create an endpoint using a `.pfx` certificate: @@ -66,7 +100,7 @@ The below example will create an endpoint using a `.pfx` certificate: Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -Certificate './certs/example.pfx' -CertificatePassword 'hunter2' ``` -Whereas the following will instead create an X509Certificate, and pass that to the endpoint instead: +However, the following will instead create an X509Certificate, and pass that to the endpoint instead: ```powershell $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new('./certs/example.cer') @@ -81,10 +115,10 @@ Add-PodeEndpoint -Address * -Port 8443 -Protocol Https -SelfSigned ### SSL Protocols -By default Pode will use the SSL3 or TLS12 protocols - or just TLS12 if on MacOS. You can override this default in one of two ways: +By default, Pode will use the SSL3 or TLS12 protocols - or just TLS12 if on MacOS. You can override this default in one of two ways: 1. Update the global default in Pode's configuration file, as [described here](../../Certificates#ssl-protocols). -2. Specify specific SSL Protocols to use per Endpoints using the `-SslProtocol` parameter on [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint). +2. Specify specific SSL Protocols to use per Endpoint using the `-SslProtocol` parameter on [`Add-PodeEndpoint`](../../../Functions/Core/Add-PodeEndpoint). ## Endpoint Names @@ -124,19 +158,19 @@ Get-PodeEndpoint -Name Admin, User The following is the structure of the Endpoint object internally, as well as the object that is returned from [`Get-PodeEndpoint`](../../../Functions/Core/Get-PodeEndpoint): -| Name | Type | Description | -| ---- | ---- | ----------- | -| Name | string | The name of the Endpoint, if a name was supplied | -| Description | string | A description of the Endpoint, usually used for OpenAPI | -| Address | IPAddress | The IP address that will be used for the Endpoint | -| RawAddress | string | The address/host and port of the Endpoint | -| Port | int | The port the Endpoint will use | -| IsIPAddress | bool | Whether or not the listener will bind using Hostname or IP address | -| Hostname | string | The hostname of the Endpoint | -| FriendlyName | string | A user friendly hostname to use when generating internal URLs | -| Url | string | The full base URL of the Endpoint | -| Ssl.Enabled | bool | Whether or not this Endpoint uses SSL | +| Name | Type | Description | +| ------------- | ------------ | ------------------------------------------------------------------------------- | +| Name | string | The name of the Endpoint, if a name was supplied | +| Description | string | A description of the Endpoint, usually used for OpenAPI | +| Address | IPAddress | The IP address that will be used for the Endpoint | +| RawAddress | string | The address/host and port of the Endpoint | +| Port | int | The port the Endpoint will use | +| IsIPAddress | bool | Whether or not the listener will bind using Hostname or IP address | +| Hostname | string | The hostname of the Endpoint | +| FriendlyName | string | A user friendly hostname to use when generating internal URLs | +| Url | string | The full base URL of the Endpoint | +| Ssl.Enabled | bool | Whether or not this Endpoint uses SSL | | Ssl.Protocols | SslProtocols | An aggregated integer which specifies the SSL protocols this endpoints supports | -| Protocol | string | The protocol of the Endpoint. Such as: HTTP, HTTPS, WS, etc. | -| Type | string | The type of the Endpoint. Such as: HTTP, WS, SMTP, TCP | -| Certificate | hashtable | Details about the certificate that will be used for SSL Endpoints | +| Protocol | string | The protocol of the Endpoint. Such as: HTTP, HTTPS, WS, etc. | +| Type | string | The type of the Endpoint. Such as: HTTP, WS, SMTP, TCP | +| Certificate | hashtable | Details about the certificate that will be used for SSL Endpoints | diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index ab727c264..da5a640d4 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -358,7 +358,7 @@ function Resolve-PodeIPDualMode { } # check loopbacks - if ($IP -eq [ipaddress]::Loopback) { + if (($IP -eq [ipaddress]::Loopback) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) { return @($IP, [ipaddress]::IPv6Loopback) } @@ -367,7 +367,7 @@ function Resolve-PodeIPDualMode { } # if iIPv4, convert and return both - if ($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) { + if (($IP.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork) -and [System.Net.Sockets.Socket]::OSSupportsIPv6) { return @($IP, $IP.MapToIPv6()) } From f499ce7e4109601979975ce410b06fa14e372fd3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 14:38:14 -0700 Subject: [PATCH 36/84] File Browsing feature for Pode Static Route #1237 Implementation of File Browser for static route + documentation --- docs/Tutorials/Configuration.md | 29 +- .../Routes/Utilities/StaticContent.md | 37 ++ examples/FileBrowser/FileBrowser.ps1 | 58 +++ examples/FileBrowser/public/ruler.png | Bin 0 -> 2708 bytes src/Misc/default-file-browsing.html.pode | 225 +++++++++ src/Pode.psd1 | 1 + src/Private/Helpers.ps1 | 48 +- src/Private/Middleware.ps1 | 32 +- src/Private/PodeServer.ps1 | 11 +- src/Private/Responses.ps1 | 459 ++++++++++++++++++ src/Private/Serverless.ps1 | 11 +- src/Public/Core.ps1 | 8 +- src/Public/Responses.ps1 | 157 +++--- src/Public/Routes.ps1 | 28 +- 14 files changed, 944 insertions(+), 160 deletions(-) create mode 100644 examples/FileBrowser/FileBrowser.ps1 create mode 100644 examples/FileBrowser/public/ruler.png create mode 100644 src/Misc/default-file-browsing.html.pode diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index 7e91cfa0b..6e38b2876 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -68,17 +68,18 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: } ``` -| Path | Description | Docs | -| ---- | ----------- | ---- | -| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | -| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | -| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | -| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) | -| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | -| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | -| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | -| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | -| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | -| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | -| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | -| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | +| Path | Description | Docs | +| --------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------ | +| Server.Ssl.Protocols | Indicates the SSL Protocols that should be used | [link](../Certificates) | +| Server.Request | Defines request timeout and maximum body size | [link](../RequestLimits) | +| Server.AutoImport | Defines the AutoImport scoping rules for Modules, SnapIns and Functions | [link](../Scoping) | +| Server.Logging | Defines extra configuration for Logging, like masking sensitive data | [link](../Logging/Overview) | +| Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | +| Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | +| Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | +| Server.RouteOrderMainBeforeStatic | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | +| Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | +| Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | +| Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | +| Web.ErrorPages | Defines configuration for custom error pages | [link](../Routes/Utilities/ErrorPages) | +| Web.Static | Defines configuration for static content, such as caching | [link](../Routes/Utilities/StaticContent) | diff --git a/docs/Tutorials/Routes/Utilities/StaticContent.md b/docs/Tutorials/Routes/Utilities/StaticContent.md index 44da45d17..edfedc5f0 100644 --- a/docs/Tutorials/Routes/Utilities/StaticContent.md +++ b/docs/Tutorials/Routes/Utilities/StaticContent.md @@ -168,3 +168,40 @@ Start-PodeServer { ``` When a static route is set as downloadable, then `-Defaults` and caching are not used. + +## File Browsing + +This feature allows the use of a static route as an HTML file browser. If you set the `-FileBrowser` switch on the [`Add-PodeStaticRoute`] function, the route will show the folder content whenever it is invoked. + +```powershell +Start-PodeServer -ScriptBlock { + Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http + Add-PodeStaticRoute -Path '/' -Source './content/assets' -FileBrowser + Add-PodeStaticRoute -Path '/download' -Source './content/newassets' -DownloadOnly -FileBrowser +} +``` + +When used with `-Download,` the browser downloads any file selected instead of rendering. The folders are rendered and not downloaded. + +## Static Routes order +By default, Static routes are processed before any other route. +There are situations where you want a main `GET` route has the priority to a static one. +For example, you have to hide or make some computation to a file or a folder before returning the result. + +```powershell +Start-PodeServer -ScriptBlock { + Add-PodeRoute -Method Get -Path '/LICENSE.txt' -ScriptBlock { + $value = @' +Don't kidding me. Nobody will believe that you want to read this legalise nonsense. +I want to be kind; this is a summary of the content: + +Nothing to report :D +'@ + Write-PodeTextResponse -Value $value + } + + Add-PodeStaticRoute -Path '/' -Source "./content" -FileBrowser +} +``` + +To change the default behavior, you can use the `Server.RouteOrderMainBeforeStatic` property in the `server.psd1` configuration file, setting the value to `$True.` This will ensure that any static route is evaluated after any other route. \ No newline at end of file diff --git a/examples/FileBrowser/FileBrowser.ps1 b/examples/FileBrowser/FileBrowser.ps1 new file mode 100644 index 000000000..517f459e3 --- /dev/null +++ b/examples/FileBrowser/FileBrowser.ps1 @@ -0,0 +1,58 @@ +$FileBrowserPath = Split-Path -Parent -Path $MyInvocation.MyCommand.Path +$podePath = Split-Path -Parent -Path (Split-Path -Parent -Path $FileBrowserPath) +if (Test-Path -Path "$($podePath)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($podePath)/src/Pode.psm1" -Force -ErrorAction Stop +} +else { + Import-Module -Name 'Pode' +} + +$directoryPath = $podePath +# Start Pode server +Start-PodeServer -ScriptBlock { + + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http -Default + + New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + # setup basic auth (base64> username:password in header) + New-PodeAuthScheme -Basic -Realm 'Pode Static Page' | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { + param($username, $password) + + # here you'd check a real user storage, this is just for example + if ($username -eq 'morty' -and $password -eq 'pickle') { + return @{ + User = @{ + ID = 'M0R7Y302' + Name = 'Morty' + Type = 'Human' + } + } + } + + return @{ Message = 'Invalid details supplied' } + } + Add-PodeRoute -Method Get -Path '/LICENSE.txt' -ScriptBlock { + $value = @' +Don't kidding me. Nobody will believe that you want to read this legalise nonsense. +I want to be kind; this is a summary of the content: + +Nothing to report :D +'@ + Write-PodeTextResponse -Value $value + } + Add-PodeStaticRouteGroup -FileBrowser -Routes { + + Add-PodeStaticRoute -Path '/' -Source $using:directoryPath + Add-PodeStaticRoute -Path '/download' -Source $using:directoryPath -DownloadOnly + Add-PodeStaticRoute -Path '/nodownload' -Source $using:directoryPath + Add-PodeStaticRoute -Path '/any/*/test' -Source $using:directoryPath + Add-PodeStaticRoute -Path '/auth' -Source $using:directoryPath -Authentication 'Validate' + } + Add-PodeStaticRoute -Path '/nobrowsing' -Source $directoryPath + + Add-PodeRoute -Method Get -Path '/attachment/*/test' -ScriptBlock { + Set-PodeResponseAttachment -Path 'ruler.png' + } +} diff --git a/examples/FileBrowser/public/ruler.png b/examples/FileBrowser/public/ruler.png new file mode 100644 index 0000000000000000000000000000000000000000..4dff9733927494feae7f8c7a1b3c521c6ae554ba GIT binary patch literal 2708 zcmbVOX;c&E8jcExED{84MU*iJ0-9v9lPCzuBob@bHz`#@GLQgSNTMVt3J3y%MJlzZ zaRaYH#0^1_MK%@CUdrNvP^7f7l%*ho3TVASrT6~m>5n_-%zWSbeed(U`#F>D<*~)s zz|sJLKp3;#As_7u&b|72+Hc^(=~V5qLct7D_)5YRv0NF9aN|qDV1O;+M!-HWm#>Ox zf;S-$i^7F|L5d*GHX2VN!f@wwFiMeBi$)+eZB|OTyeL=!guxL)u`_C{q8bGV`Oc_7 z5(nf+8L&X;9w&o+<2?L$aZx-fAGO&9*rcRs2}H1h3n)d=VmVFejQYq+)9&Yvu_)l9 ziXzGx^~tFqju*g?$Y6kk0nt2=3<6{v21g)LD3lEV9>jrI5Rb*-(Krf?fTw{V@cBV$ zt;zV|G#`ld*_KvvMhO%ODGiH_jg7^`5-<{31Qti7Qs+4Ec(hgnEmw&ZTqRm8x0z>v zU^!1Flq!T0F)+u-4U_CrIHRZYc-ne|+suQtx%+2BbBbDTXr3~mwkX_aNW$ACg2f6phszU+=j6Hae2Nx=Wy0MspCyxsfRFj53IE2v)?JWAfmn1mJVCbiw<& zU58z~c*f@>@$pTU$*^b*`N{oG$T!N?(ZAO40m`tf=6S?GTaVEVa1Zxc$~U#=kE+$T z)r42G>Yh!}F&h{VG>o89qQ|?v~oX%K_-{^09UHN)iC)?;O zM?&0{6F{mJG}V?G4tU*)Lt94V;3tQ;6BHlWK+7_{|vGWtUPTR zLfvEV&}w~489b|g3l*7IWu_oWbx~1I&N8ZBVKhTK6IQUCBq3+a&bXVW{w_UGGi%$MmGE??d3omM#EO4s*ZWcsC*uIEzD4Tm6ukwc!~3$rVKh?SLh z*6C$$Fl%sg#~~k-0^fNWIk@!hu8@wt+y3aPirJd1zJ9|vTY-THcXEIid4%EKeIxY4 zzcB}sbowllpISGT^E@}MJe-PL2b5%QjUeYG72mq2z65C=Ui8c>DgU5X9u?6KHn!6Wu*Fkl+xBy{{#;)1C&ZM%zu*p5EE<( zy;iC7HT(E?>+sj@!y-CkBGcGA(;!C|_x}7F_GVqp@`zu`yAI4e4JH&9^CH@}pH|tW zcnyQks_pd$hlg4A$eM{6i%>zLUj_G7(VC8D8qi;#u>*5x{SW6fH&-AVYbD)v)4ZMM z;=1o|ce{36dfpFk$}-8@dE{1}z@qN#K##|U%fjSDsGzG%SMQJqAu*I-(R|^67kxb} znC_g+JhiOb^2M_+YF@RZS9`oee8p|eB-V!G3n4dYNS$tW$n0w4qrB8wL$2bRDjm58 zMsxD(9Aj$y8S;n+*IPX=jD2?rlXS#ZvuSXaCdv`t>?5t(c-@N)f|7wDB; zb-D$@ko1<8f@1i$safAP=n3LGIwXYh?jaX{sNvZL8X3RNAO2OBShW43AdHq)v~Zi@ z8V=n)${-{CSiVhxXov3G`XTZKY#|k11*}HpAi}JYNkd*NLZI1YYlvObQ~hVsMBHLa zisV2_!0t)S5OqP?!9foMZXZT+ddY)4{l;_pkIRPOpsCyAo1~86tSzC<2%i+M$O+vZ zn_%F&E@SbZSR@p&qj~zw=jif^NP0)8ed3%A2_<7BFM?CS=# z_5N9pn5`A0P>NRceQI;t|GZP1Q-Wx-8F~xW4LNa<&Pc zdPn^1-d5l~6fbIRcvKWT{HxzZ)1eH?^8H65N=wl8`e3OH=N}xLm-&^B zce+g6va!oCceMeCyQ0mHW_Y9R{FMiSQ9n&ZqJ;>^-?h)FB&PRu@!t9?srm1&S6$T0 zxp4jO4hHbC*~e;v<7n%R_jp!Kw<@JCFJHQOSgRo{B*xrwLAduS?ah;XVipk;4H zR_4BP7r^!!1D8l0x9lyo3?t@S&#rd3aO(Bxg4TsscYGzJ#v~ux<{(2yzH@8IycPh4 vTaMl{$jW^K;uaAWm~Wi%b{Q{O{b31+pgz~U$zU + + + + + + File Browser:$($Data.Path) + + + + + + + + + +
+
+
+ + + + $($Data.fileContent) +
+

+ 🧡 Powered by Pode +

+
+ + + + \ No newline at end of file diff --git a/src/Pode.psd1 b/src/Pode.psd1 index 72fef9247..dfae68cd8 100644 --- a/src/Pode.psd1 +++ b/src/Pode.psd1 @@ -77,6 +77,7 @@ 'Write-PodeJsonResponse', 'Write-PodeXmlResponse', 'Write-PodeViewResponse', + 'Write-PodeDirectoryResponse', 'Set-PodeResponseStatus', 'Move-PodeResponseUrl', 'Write-PodeTcpClient', diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index ecc38dc86..5ed49ee76 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1619,23 +1619,6 @@ function Get-PodeCount { return $Object.Count } -function Test-PodePathAccess { - param( - [Parameter(Mandatory = $true)] - [string] - $Path - ) - - try { - $null = Get-Item $Path - } - catch [System.UnauthorizedAccessException] { - return $false - } - - return $true -} - function Test-PodePath { param( [Parameter()] @@ -1647,35 +1630,20 @@ function Test-PodePath { [switch] $FailOnDirectory ) - - # if the file doesnt exist then fail on 404 - if ([string]::IsNullOrWhiteSpace($Path) -or !(Test-Path $Path)) { - if (!$NoStatus) { - Set-PodeResponseStatus -Code 404 + if (![string]::IsNullOrWhiteSpace($Path)) { + $item = Get-Item $Path -ErrorAction Ignore + if ($null -ne $item -and (! $FailOnDirectory.IsPresent -or !$item.PSIsContainer)) { + return $true } - - return $false } - # if the file isn't accessible then fail 401 - if (!(Test-PodePathAccess $Path)) { - if (!$NoStatus) { - Set-PodeResponseStatus -Code 401 - } - + # if the file doesnt exist then fail on 404 + if ($NoStatus.IsPresent) { return $false } - - # if we're failing on a directory then fail on 404 - if ($FailOnDirectory -and (Test-PodePathIsDirectory $Path)) { - if (!$NoStatus) { - Set-PodeResponseStatus -Code 404 - } - - return $false + else { + Set-PodeResponseStatus -Code 404 } - - return $true } function Test-PodePathIsFile { diff --git a/src/Private/Middleware.ps1 b/src/Private/Middleware.ps1 index 8db9d392e..24c7099cf 100644 --- a/src/Private/Middleware.ps1 +++ b/src/Private/Middleware.ps1 @@ -195,14 +195,40 @@ function Get-PodePublicMiddleware { }) } +<# +.SYNOPSIS + Middleware function to validate the route for an incoming web request. + +.DESCRIPTION + This function is used as middleware to validate the route for an incoming web request. It checks if the route exists for the requested method and path. If the route does not exist, it sets the appropriate response status code (404 for not found, 405 for method not allowed) and returns false to halt further processing. If the route exists, it sets various properties on the WebEvent object, such as parameters, content type, and transfer encoding, and returns true to continue processing. + +.PARAMETER None + +.EXAMPLE + $middleware = Get-PodeRouteValidateMiddleware + Add-PodeMiddleware -Middleware $middleware + +.NOTES + This function is part of the internal Pode server logic and is typically not called directly by users. + +#> function Get-PodeRouteValidateMiddleware { return @{ Name = '__pode_mw_route_validation__' Logic = { - # check if the path is static route first, then check the main routes - $route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name - if ($null -eq $route) { + if ($Server.Configuration.Server.RouteOrderMainBeforeStatic) { + # check the main routes and check the static routes $route = Find-PodeRoute -Method $WebEvent.Method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name -CheckWildMethod + if ($null -eq $route) { + $route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name + } + } + else { + # check if the path is static route first, then check the main routes + $route = Find-PodeStaticRoute -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name + if ($null -eq $route) { + $route = Find-PodeRoute -Method $WebEvent.Method -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name -CheckWildMethod + } } # if there's no route defined, it's a 404 - or a 405 if a route exists for any other method diff --git a/src/Private/PodeServer.ps1 b/src/Private/PodeServer.ps1 index 40596694e..877aa091c 100644 --- a/src/Private/PodeServer.ps1 +++ b/src/Private/PodeServer.ps1 @@ -208,8 +208,9 @@ function Start-PodeWebServer { # invoke the route if ($null -ne $WebEvent.StaticContent) { + $fileBrowser = $WebEvent.Route.FileBrowser if ($WebEvent.StaticContent.IsDownload) { - Set-PodeResponseAttachment -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name + Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser } elseif ($WebEvent.StaticContent.RedirectToDefault) { $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) @@ -217,11 +218,13 @@ function Start-PodeWebServer { } else { $cachable = $WebEvent.StaticContent.IsCachable - Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable + Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge ` + -Cache:$cachable -FileBrowser:$fileBrowser } } elseif ($null -ne $WebEvent.Route.Logic) { - $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat + $null = Invoke-PodeScriptBlock -ScriptBlock $WebEvent.Route.Logic -Arguments $WebEvent.Route.Arguments ` + -UsingVariables $WebEvent.Route.UsingVariables -Scoped -Splat } } } @@ -472,6 +475,7 @@ function Start-PodeWebServer { function New-PodeListener { [CmdletBinding()] + [OutputType([Pode.PodeListener])] param( [Parameter(Mandatory = $true)] [System.Threading.CancellationToken] @@ -483,6 +487,7 @@ function New-PodeListener { function New-PodeListenerSocket { [CmdletBinding()] + [OutputType([Pode.PodeSocket])] param( [Parameter(Mandatory = $true)] [ipaddress] diff --git a/src/Private/Responses.ps1 b/src/Private/Responses.ps1 index e350c8d5a..2e0616ca9 100644 --- a/src/Private/Responses.ps1 +++ b/src/Private/Responses.ps1 @@ -1,3 +1,37 @@ +<# +.SYNOPSIS +Displays a customized error page based on the provided error code and additional error details. + +.DESCRIPTION +This function is responsible for displaying a custom error page when an error occurs within a Pode web application. It takes an error code, a description, an exception object, and a content type as input. The function then attempts to find a corresponding error page based on the error code and content type. If a custom error page is found, and if exception details are to be shown (as per server settings), it builds a detailed exception message. Finally, it writes the error page to the response stream, displaying the custom error page to the user. + +.PARAMETER Code +The HTTP status code of the error. This code is used to find a matching custom error page. + +.PARAMETER Description +A descriptive message about the error. This is displayed on the error page if available. + +.PARAMETER Exception +The exception object that caused the error. If exception tracing is enabled, details from this object are displayed on the error page. + +.PARAMETER ContentType +The content type of the error page to be displayed. This is used to select an appropriate error page format (e.g., HTML, JSON). + +.EXAMPLE +Show-PodeErrorPage -Code 404 -Description "Not Found" -ContentType "text/html" + +This example shows how to display a custom 404 Not Found error page in HTML format. + +.OUTPUTS +None. This function writes the error page directly to the response stream. + +.NOTES +- The function uses `Find-PodeErrorPage` to locate a custom error page based on the HTTP status code and content type. +- It checks for server configuration to determine whether to show detailed exception information on the error page. +- The function relies on the global `$PodeContext` variable for server settings and to encode exception and URL details safely. +- `Write-PodeFileResponse` is used to send the custom error page as the response, along with any dynamic data (e.g., exception details, URL). +- This is an internal function and may change in future releases of Pode. +#> function Show-PodeErrorPage { param( [Parameter()] @@ -48,4 +82,429 @@ function Show-PodeErrorPage { # write the error page to the stream Write-PodeFileResponse -Path $errorPage.Path -Data $data -ContentType $errorPage.ContentType +} + + + +<# +.SYNOPSIS +Serves files as HTTP responses in a Pode web server, handling both dynamic and static content. + +.DESCRIPTION +This function serves files from the server to the client, supporting both static files and files that are dynamically processed by a view engine. +For dynamic content, it uses the server's configured view engine to process the file and returns the rendered content. +For static content, it simply returns the file's content. The function allows for specifying content type, cache control, and HTTP status code. + +.PARAMETER Path +The relative path to the file to be served. This path is resolved against the server's root directory. + +.PARAMETER Data +A hashtable of data that can be passed to the view engine for dynamic files. + +.PARAMETER ContentType +The MIME type of the response. If not provided, it is inferred from the file extension. + +.PARAMETER MaxAge +The maximum age (in seconds) for which the response can be cached by the client. Applies only to static content. + +.PARAMETER StatusCode +The HTTP status code to accompany the response. Defaults to 200 (OK). + +.PARAMETER Cache +A switch to indicate whether the response should include HTTP caching headers. Applies only to static content. + +.EXAMPLE +Write-PodeFileResponseInternal -Path 'index.pode' -Data @{ Title = 'Home Page' } -ContentType 'text/html' + +Serves the 'index.pode' file as an HTTP response, processing it with the view engine and passing in a title for dynamic content rendering. + +.EXAMPLE +Write-PodeFileResponseInternal -Path 'logo.png' -ContentType 'image/png' -Cache + +Serves the 'logo.png' file as a static file with the specified content type and caching enabled. + +.OUTPUTS +None. The function writes directly to the HTTP response stream. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> + +function Write-PodeFileResponseInternal { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNull()] + [string] + $Path, + + [Parameter()] + $Data = @{}, + + [Parameter()] + [string] + $ContentType = $null, + + [Parameter()] + [int] + $MaxAge = 3600, + + [Parameter()] + [int] + $StatusCode = 200, + + [switch] + $Cache, + + [switch] + $FileBrowser + ) + + # Attempt to retrieve information about the path + $pathInfo = Get-Item -Path $Path -force -ErrorAction Continue + + # Check if the path exists + if ($null -eq $pathInfo) { + # If not, set the response status to 404 Not Found + Set-PodeResponseStatus -Code 404 + } + else { + # Check if the path is a directory + if ( $pathInfo.PSIsContainer) { + # If directory browsing is enabled, use the directory response function + if ($FileBrowser.isPresent) { + Write-PodeDirectoryResponseInternal -Path $Path + } + else { + # If browsing is not enabled, return a 404 error + Set-PodeResponseStatus -Code 404 + } + } + else { + + # are we dealing with a dynamic file for the view engine? (ignore html) + # Determine if the file is dynamic and should be processed by the view engine + $mainExt = Get-PodeFileExtension -Path $Path -TrimPeriod + + # generate dynamic content + if (![string]::IsNullOrWhiteSpace($mainExt) -and ( + ($mainExt -ieq 'pode') -or + ($mainExt -ieq $PodeContext.Server.ViewEngine.Extension -and $PodeContext.Server.ViewEngine.IsDynamic) + ) + ) { + # Process dynamic content with the view engine + $content = Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data + + # Determine the correct content type for the response + # get the sub-file extension, if empty, use original + $subExt = Get-PodeFileExtension -Path (Get-PodeFileName -Path $Path -WithoutExtension) -TrimPeriod + $subExt = (Protect-PodeValue -Value $subExt -Default $mainExt) + + $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $subExt)) + # Write the processed content as the HTTP response + Write-PodeTextResponse -Value $content -ContentType $ContentType -StatusCode $StatusCode + } + # this is a static file + else { + if (Test-PodeIsPSCore) { + $content = (Get-Content -Path $Path -Raw -AsByteStream) + } + else { + $content = (Get-Content -Path $Path -Raw -Encoding byte) + } + if ($null -ne $content) { + # Determine and set the content type for static files + $ContentType = Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $mainExt) + # Write the file content as the HTTP response + Write-PodeTextResponse -Bytes $content -ContentType $ContentType -MaxAge $MaxAge -StatusCode $StatusCode -Cache:$Cache + } + else { + # If the file does not exist, set the HTTP response status to 404 Not Found + Set-PodeResponseStatus -Code 404 + } + } + } + } +} + +<# +.SYNOPSIS +Serves a directory listing as a web page. + +.DESCRIPTION +The Write-PodeDirectoryResponseInternal function generates an HTML response that lists the contents of a specified directory, +allowing for browsing of files and directories. It supports both Windows and Unix-like environments by adjusting the +display of file attributes accordingly. If the path is a directory, it generates a browsable HTML view; otherwise, it +serves the file directly. + +.PARAMETER Path +The relative path to the directory that should be displayed. This path is resolved and used to generate a list of contents. + + +.EXAMPLE +# resolve for relative path +$RelativePath = Get-PodeRelativePath -Path './static' -JoinRoot +Write-PodeDirectoryResponseInternal -Path './static' + +Generates and serves an HTML page that lists the contents of the './static' directory, allowing users to click through files and directories. + +.NOTES +This is an internal function and may change in future releases of Pode. +#> +function Write-PodeDirectoryResponseInternal { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [ValidateNotNull()] + [string] + $Path + ) + + try { + if ($WebEvent.Path -eq '/') { + $leaf = '/' + $rootPath = '/' + } + else { + # get leaf of current physical path, and set root path + $leaf = ($Path.Split(':')[1] -split '[\\/]+') -join '/' + $rootPath = $WebEvent.Path -ireplace "$($leaf)$", '' + } + + # Determine if the server is running in Windows mode or is running a varsion that support Linux + # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-childitem?view=powershell-7.4#example-10-output-for-non-windows-operating-systems + $windowsMode = ((Test-PodeIsWindows) -or ($PSVersionTable.PSVersion -lt [version]'7.1.0') ) + + # Construct the HTML content for the file browser view + $htmlContent = [System.Text.StringBuilder]::new() + + $atoms = $WebEvent.Path -split '/' + $atoms = @(foreach ($atom in $atoms) { + if (![string]::IsNullOrEmpty($atom)) { + [uri]::EscapeDataString($atom) + } + }) + if ([string]::IsNullOrWhiteSpace($atoms)) { + $baseLink = '' + } + else { + $baseLink = "/$($atoms -join '/')" + } + + # Handle navigation to the parent directory (..) + if ($leaf -ne '/') { + $LastSlash = $baseLink.LastIndexOf('/') + if ($LastSlash -eq -1) { + Set-PodeResponseStatus -Code 404 + return + } + $ParentLink = $baseLink.Substring(0, $LastSlash) + if ([string]::IsNullOrWhiteSpace($ParentLink)) { + $ParentLink = '/' + } + $item = Get-Item '..' + if ($windowsMode) { + $htmlContent.Append(" ") + $htmlContent.Append($item.Mode) + } + else { + $htmlContent.Append(" ") + $htmlContent.Append($item.UnixMode) + $htmlContent.Append(" ") + $htmlContent.Append($item.User) + $htmlContent.Append(" ") + $htmlContent.Append($item.Group) + } + $htmlContent.Append(" ") + $htmlContent.Append($item.CreationTime.ToString('yyyy-MM-dd HH:mm:ss')) + $htmlContent.Append(" ") + $htmlContent.Append($item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) + $htmlContent.Append( " .. ") + } + # Retrieve the child items of the specified directory + $child = Get-ChildItem -Path $Path -Force + foreach ($item in $child) { + $link = "$baseLink/$([uri]::EscapeDataString($item.Name))" + if ($item.PSIsContainer) { + $size = '' + $icon = 'bi bi-folder2' + } + else { + $size = '{0:N2}KB' -f ($item.Length / 1KB) + $icon = 'bi bi-file' + } + + # Format each item as an HTML row + if ($windowsMode) { + $htmlContent.Append(" ") + $htmlContent.Append($item.Mode) + } + else { + $htmlContent.Append(" ") + $htmlContent.Append($item.UnixMode) + $htmlContent.Append(" ") + $htmlContent.Append($item.User) + $htmlContent.Append(" ") + $htmlContent.Append($item.Group) + } + $htmlContent.Append(" ") + $htmlContent.Append($item.CreationTime.ToString('yyyy-MM-dd HH:mm:ss')) + $htmlContent.Append(" ") + $htmlContent.Append($item.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss')) + $htmlContent.Append(" ") + $htmlContent.Append( $size) + $htmlContent.Append( " ") + $htmlContent.Append($item.Name ) + $htmlContent.AppendLine(' ' ) + } + + $Data = @{ + RootPath = $RootPath + Path = $leaf.Replace('\', '/') + WindowsMode = $windowsMode.ToString().ToLower() + FileContent = $htmlContent.ToString() # Convert the StringBuilder content to a string + } + + $podeRoot = Get-PodeModuleMiscPath + # Write the response + Write-PodeFileResponseInternal -Path ([System.IO.Path]::Combine($podeRoot, 'default-file-browsing.html.pode')) -Data $Data + } + catch { + write-podehost $_ + } +} + + + +<# +.SYNOPSIS +Sends a file as an attachment in the response, supporting both file streaming and directory browsing options. + +.DESCRIPTION +The Write-PodeAttachmentResponseInternal function is designed to handle HTTP responses for file downloads or directory browsing within a Pode web server. It resolves the given file or directory path, sets the appropriate content type, and configures the response to either download the file as an attachment or list the directory contents if browsing is enabled. The function supports both PowerShell Core and Windows PowerShell environments for file content retrieval. + +.PARAMETER Path +The path to the file or directory. This parameter is mandatory and accepts pipeline input. The function resolves relative paths based on the server's root directory. + +.PARAMETER ContentType +The MIME type of the file being served. This is validated against a pattern to ensure it's in the format 'type/subtype'. If not specified, the function attempts to determine the content type based on the file extension. + +.PARAMETER FileBrowser +A switch parameter that, when present, enables directory browsing. If the path points to a directory and this parameter is enabled, the function will list the directory's contents instead of returning a 404 error. + +.EXAMPLE +Write-PodeAttachmentResponseInternal -Path './files/document.pdf' -ContentType 'application/pdf' + +Serves the 'document.pdf' file with the 'application/pdf' MIME type as a downloadable attachment. + +.EXAMPLE +Write-PodeAttachmentResponseInternal -Path './files' -FileBrowser + +Lists the contents of the './files' directory if the FileBrowser switch is enabled; otherwise, returns a 404 error. + +.NOTES +- This function integrates with Pode's internal handling of HTTP responses, leveraging other Pode-specific functions like Get-PodeContentType and Set-PodeResponseStatus. It differentiates between streamed and serverless environments to optimize file delivery. +- This is an internal function and may change in future releases of Pode. +#> +function Write-PodeAttachmentResponseInternal { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] + $Path, + + [Parameter()] + [string] + $ContentType, + + [Parameter()] + [switch] + $FileBrowser + + ) + + # resolve for relative path + $Path = Get-PodeRelativePath -Path $Path -JoinRoot + + # Attempt to retrieve information about the path + $pathInfo = Get-Item -Path $Path -force -ErrorAction Continue + # Check if the path exists + if ($null -eq $pathInfo) { + #if not exist try with to find with public Route if exist + $Path = Find-PodePublicRoute -Path $Path + if ($Path) { + # only attach files from public/static-route directories when path is relative + $Path = Get-PodeRelativePath -Path $Path -JoinRoot + # Attempt to retrieve information about the path + $pathInfo = Get-Item -Path $Path -ErrorAction Continue + } + if ($null -eq $pathInfo) { + # If not, set the response status to 404 Not Found + Set-PodeResponseStatus -Code 404 + return + } + } + if ( $pathInfo.PSIsContainer) { + # If directory browsing is enabled, use the directory response function + if ($FileBrowser.isPresent) { + Write-PodeDirectoryResponseInternal -Path $Path + return + } + else { + # If browsing is not enabled, return a 404 error + Set-PodeResponseStatus -Code 404 + return + } + } + try { + # setup the content type and disposition + if (!$ContentType) { + $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $pathInfo.Extension) + } + else { + $WebEvent.Response.ContentType = $ContentType + } + + Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($pathInfo.Name)" + + # if serverless, get the content raw and return + if (!$WebEvent.Streamed) { + if (Test-PodeIsPSCore) { + $content = (Get-Content -Path $Path -Raw -AsByteStream) + } + else { + $content = (Get-Content -Path $Path -Raw -Encoding byte) + } + + $WebEvent.Response.Body = $content + } + + # else if normal, stream the content back + else { + # setup the response details and headers + $WebEvent.Response.SendChunked = $false + + # set file as an attachment on the response + $buffer = [byte[]]::new(64 * 1024) + $read = 0 + + # open up the file as a stream + $fs = (Get-Item $Path).OpenRead() + $WebEvent.Response.ContentLength64 = $fs.Length + + while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) { + $WebEvent.Response.OutputStream.Write($buffer, 0, $read) + } + } + } + finally { + Close-PodeDisposable -Disposable $fs + } + } \ No newline at end of file diff --git a/src/Private/Serverless.ps1 b/src/Private/Serverless.ps1 index 0ff62e4fa..a7805947c 100644 --- a/src/Private/Serverless.ps1 +++ b/src/Private/Serverless.ps1 @@ -83,8 +83,9 @@ function Start-PodeAzFuncServer { if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) { # invoke the route if ($null -ne $WebEvent.StaticContent) { + $fileBrowser = $WebEvent.Route.FileBrowser if ($WebEvent.StaticContent.IsDownload) { - Set-PodeResponseAttachment -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name + Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser } elseif ($WebEvent.StaticContent.RedirectToDefault) { $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) @@ -92,7 +93,7 @@ function Start-PodeAzFuncServer { } else { $cachable = $WebEvent.StaticContent.IsCachable - Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable + Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable -FileBrowser:$fileBrowser } } else { @@ -196,8 +197,9 @@ function Start-PodeAwsLambdaServer { if ((Invoke-PodeMiddleware -Middleware $WebEvent.Route.Middleware)) { # invoke the route if ($null -ne $WebEvent.StaticContent) { + $fileBrowser = $WebEvent.Route.FileBrowser if ($WebEvent.StaticContent.IsDownload) { - Set-PodeResponseAttachment -Path $WebEvent.Path -EndpointName $WebEvent.Endpoint.Name + Write-PodeAttachmentResponseInternal -Path $WebEvent.StaticContent.Source -FileBrowser:$fileBrowser } elseif ($WebEvent.StaticContent.RedirectToDefault) { $file = [System.IO.Path]::GetFileName($WebEvent.StaticContent.Source) @@ -205,7 +207,8 @@ function Start-PodeAwsLambdaServer { } else { $cachable = $WebEvent.StaticContent.IsCachable - Write-PodeFileResponse -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge -Cache:$cachable + Write-PodeFileResponseInternal -Path $WebEvent.StaticContent.Source -MaxAge $PodeContext.Server.Web.Static.Cache.MaxAge ` + -Cache:$cachable -FileBrowser:$fileBrowser } } else { diff --git a/src/Public/Core.ps1 b/src/Public/Core.ps1 index afe1aefc8..baf303eb7 100644 --- a/src/Public/Core.ps1 +++ b/src/Public/Core.ps1 @@ -306,6 +306,9 @@ An array of default pages to display, such as 'index.html'. .PARAMETER DownloadOnly When supplied, all static content on this Route will be attached as downloads - rather than rendered. +.PARAMETER FileBrowser +When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory. + .PARAMETER Browse Open the web server's default endpoint in your default browser. @@ -368,6 +371,9 @@ function Start-PodeStaticServer { [switch] $DownloadOnly, + [switch] + $FileBrowser, + [switch] $Browse ) @@ -390,7 +396,7 @@ function Start-PodeStaticServer { } # add the static route - Add-PodeStaticRoute -Path $Path -Source (Get-PodeServerPath) -Defaults $Defaults -DownloadOnly:$DownloadOnly + Add-PodeStaticRoute -Path $Path -Source (Get-PodeServerPath) -Defaults $Defaults -DownloadOnly:$DownloadOnly -FileBrowser:$FileBrowser } } diff --git a/src/Public/Responses.ps1 b/src/Public/Responses.ps1 index 8fff8c91a..97690d265 100644 --- a/src/Public/Responses.ps1 +++ b/src/Public/Responses.ps1 @@ -18,6 +18,9 @@ The supplied value must match the valid ContentType format, e.g. application/jso .PARAMETER EndpointName Optional EndpointName that the static route was creating under. +.PARAMETER FileBrowser +If the path is a folder, instead of returning 404, will return A browsable content of the directory. + .EXAMPLE Set-PodeResponseAttachment -Path 'downloads/installer.exe' @@ -33,9 +36,10 @@ Set-PodeResponseAttachment -Path './data.txt' -ContentType 'application/json' .EXAMPLE Set-PodeResponseAttachment -Path '/assets/data.txt' -EndpointName 'Example' #> + function Set-PodeResponseAttachment { [CmdletBinding()] - param( + param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [string] $Path, @@ -46,7 +50,11 @@ function Set-PodeResponseAttachment { [Parameter()] [string] - $EndpointName + $EndpointName, + + [switch] + $FileBrowser + ) # already sent? skip @@ -55,71 +63,19 @@ function Set-PodeResponseAttachment { } # only attach files from public/static-route directories when path is relative - $_path = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName).Content.Source + $route = (Find-PodeStaticRoute -Path $Path -CheckPublic -EndpointName $EndpointName) + if ($route) { + $_path = $route.Content.Source - # if there's no path, check the original path (in case it's literal/relative) - if (!(Test-PodePath $_path -NoStatus)) { - $Path = Get-PodeRelativePath -Path $Path -JoinRoot - - if (Test-PodePath $Path -NoStatus) { - $_path = $Path - } } - - # test the file path, and set status accordingly - if (!(Test-PodePath $_path)) { - return - } - - $filename = Get-PodeFileName -Path $_path - $ext = Get-PodeFileExtension -Path $_path -TrimPeriod - - try { - # setup the content type and disposition - if (!$ContentType) { - $WebEvent.Response.ContentType = (Get-PodeContentType -Extension $ext) - } - else { - $WebEvent.Response.ContentType = $ContentType - } - - Set-PodeHeader -Name 'Content-Disposition' -Value "attachment; filename=$($filename)" - - # if serverless, get the content raw and return - if (!$WebEvent.Streamed) { - if (Test-PodeIsPSCore) { - $content = (Get-Content -Path $_path -Raw -AsByteStream) - } - else { - $content = (Get-Content -Path $_path -Raw -Encoding byte) - } - - $WebEvent.Response.Body = $content - } - - # else if normal, stream the content back - else { - # setup the response details and headers - $WebEvent.Response.SendChunked = $false - - # set file as an attachment on the response - $buffer = [byte[]]::new(64 * 1024) - $read = 0 - - # open up the file as a stream - $fs = (Get-Item $_path).OpenRead() - $WebEvent.Response.ContentLength64 = $fs.Length - - while (($read = $fs.Read($buffer, 0, $buffer.Length)) -gt 0) { - $WebEvent.Response.OutputStream.Write($buffer, 0, $read) - } - } - } - finally { - Close-PodeDisposable -Disposable $fs + else { + $_path = Get-PodeRelativePath -Path $Path -JoinRoot } + #call internal Attachment function + Write-PodeAttachmentResponseInternal -Path $_path -ContentType $ContentType -FileBrowser:$fileBrowser } + <# .SYNOPSIS Writes a String or a Byte[] to the Response. @@ -364,6 +320,9 @@ The status code to set against the response. .PARAMETER Cache Should the file's content be cached by browsers, or not? +.PARAMETER FileBrowser +If the path is a folder, instead of returning 404, will return A browsable content of the directory. + .EXAMPLE Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' @@ -378,6 +337,9 @@ Write-PodeFileResponse -Path 'C:/Views/Index.pode' -Data @{ Counter = 2 } .EXAMPLE Write-PodeFileResponse -Path 'C:/Files/Stuff.txt' -StatusCode 201 + +.EXAMPLE +Write-PodeFileResponse -Path 'C:/Files/' -FileBrowser #> function Write-PodeFileResponse { [CmdletBinding()] @@ -403,49 +365,56 @@ function Write-PodeFileResponse { $StatusCode = 200, [switch] - $Cache + $Cache, + + [switch] + $FileBrowser ) # resolve for relative path - $Path = Get-PodeRelativePath -Path $Path -JoinRoot + $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot - # test the file path, and set status accordingly - if (!(Test-PodePath $Path -FailOnDirectory)) { - return - } + Write-PodeFileResponseInternal -Path $RelativePath -Data $Data -ContentType $ContentType -MaxAge $MaxAge ` + -StatusCode $StatusCode -Cache:$Cache -FileBrowser:$FileBrowser +} - # are we dealing with a dynamic file for the view engine? (ignore html) - $mainExt = Get-PodeFileExtension -Path $Path -TrimPeriod +<# +.SYNOPSIS +Serves a directory listing as a web page. - # generate dynamic content - if (![string]::IsNullOrWhiteSpace($mainExt) -and ( - ($mainExt -ieq 'pode') -or - ($mainExt -ieq $PodeContext.Server.ViewEngine.Extension -and $PodeContext.Server.ViewEngine.IsDynamic) - )) { - $content = Get-PodeFileContentUsingViewEngine -Path $Path -Data $Data +.DESCRIPTION +The Write-PodeDirectoryResponse function generates an HTML response that lists the contents of a specified directory, +allowing for browsing of files and directories. It supports both Windows and Unix-like environments by adjusting the +display of file attributes accordingly. If the path is a directory, it generates a browsable HTML view; otherwise, it +serves the file directly. - # get the sub-file extension, if empty, use original - $subExt = Get-PodeFileExtension -Path (Get-PodeFileName -Path $Path -WithoutExtension) -TrimPeriod - $subExt = (Protect-PodeValue -Value $subExt -Default $mainExt) +.PARAMETER Path +The path to the directory that should be displayed. This path is resolved and used to generate a list of contents. - $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $subExt)) - Write-PodeTextResponse -Value $content -ContentType $ContentType -StatusCode $StatusCode - } +.EXAMPLE +Write-PodeDirectoryResponse -Path './static' - # this is a static file - else { - if (Test-PodeIsPSCore) { - $content = (Get-Content -Path $Path -Raw -AsByteStream) - } - else { - $content = (Get-Content -Path $Path -Raw -Encoding byte) - } +Generates and serves an HTML page that lists the contents of the './static' directory, allowing users to click through files and directories. +#> +function Write-PodeDirectoryResponse { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [ValidateNotNull()] + [string] + $Path + ) + + # resolve for relative path + $RelativePath = Get-PodeRelativePath -Path $Path -JoinRoot - $ContentType = (Protect-PodeValue -Value $ContentType -Default (Get-PodeContentType -Extension $mainExt)) - Write-PodeTextResponse -Bytes $content -ContentType $ContentType -MaxAge $MaxAge -StatusCode $StatusCode -Cache:$Cache + if (Test-Path -Path $RelativePath -PathType Container) { + Write-PodeDirectoryResponseInternal -Path $RelativePath + } + else { + Set-PodeResponseStatus -Code 404 } } - <# .SYNOPSIS Writes CSV data to the Response. @@ -1247,7 +1216,7 @@ function Save-PodeRequestFile { foreach ($file in $files) { # if the path is a directory, add the filename $filePath = $Path - if (Test-PodePathIsDirectory -Path $filePath) { + if (Test-Path -Path $filePath -PathType Container) { $filePath = [System.IO.Path]::Combine($filePath, $file) } diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 6f91496da..a2f468090 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -476,6 +476,9 @@ One or more optional Scopes that will be authorised to access this Route, when u .PARAMETER User One or more optional Users that will be authorised to access this Route, when using Authentication with an Access method. +.PARAMETER FileBrowser +If supplied, when the path is a folder, instead of returning 404, will return A browsable content of the directory. + .PARAMETER RedirectToDefault If supplied, the user will be redirected to the default page if found instead of the page being rendered as the folder path. @@ -563,6 +566,9 @@ function Add-PodeStaticRoute { [switch] $DownloadOnly, + [switch] + $FileBrowser, + [switch] $PassThru, @@ -620,6 +626,10 @@ function Add-PodeStaticRoute { $DownloadOnly = $RouteGroup.DownloadOnly } + if ($RouteGroup.FileBrowser) { + $FileBrowser = $RouteGroup.FileBrowser + } + if ($RouteGroup.RedirectToDefault) { $RedirectToDefault = $RouteGroup.RedirectToDefault } @@ -757,12 +767,16 @@ function Add-PodeStaticRoute { # workout a default transfer encoding for the route $TransferEncoding = Find-PodeRouteTransferEncoding -Path $Path -TransferEncoding $TransferEncoding + #The path use KleeneStar(Asterisk) + $KleeneStar = $OrigPath.Contains('*') + # add the route(s) Write-Verbose "Adding Route: [$($Method)] $($Path)" $newRoutes = @(foreach ($_endpoint in $endpoints) { @{ Source = $Source Path = $Path + KleeneStar = $KleeneStar Method = $Method Defaults = $Defaults RedirectToDefault = $RedirectToDefault @@ -785,6 +799,8 @@ function Add-PodeStaticRoute { TransferEncoding = $TransferEncoding ErrorType = $ErrorContentType Download = $DownloadOnly + IsStatic = $true + FileBrowser = $FileBrowser.isPresent OpenApi = @{ Path = $OpenApiPath Responses = @{ @@ -795,7 +811,6 @@ function Add-PodeStaticRoute { RequestBody = @{} Authentication = @() } - IsStatic = $true Metrics = @{ Requests = @{ Total = 0 @@ -1237,6 +1252,9 @@ Specifies what action to take when a Static Route already exists. (Default: Defa .PARAMETER AllowAnon If supplied, the Static Routes will allow anonymous access for non-authenticated users. +.PARAMETER FileBrowser +When supplied, If the path is a folder, instead of returning 404, will return A browsable content of the directory. + .PARAMETER DownloadOnly When supplied, all static content on the Routes will be attached as downloads - rather than rendered. @@ -1331,6 +1349,9 @@ function Add-PodeStaticRouteGroup { [switch] $AllowAnon, + [switch] + $FileBrowser, + [switch] $DownloadOnly, @@ -1399,6 +1420,10 @@ function Add-PodeStaticRouteGroup { $DownloadOnly = $RouteGroup.DownloadOnly } + if ($RouteGroup.FileBrowser) { + $FileBrowser = $RouteGroup.FileBrowser + } + if ($RouteGroup.RedirectToDefault) { $RedirectToDefault = $RouteGroup.RedirectToDefault } @@ -1442,6 +1467,7 @@ function Add-PodeStaticRouteGroup { Access = $Access AllowAnon = $AllowAnon DownloadOnly = $DownloadOnly + FileBrowser = $FileBrowser IfExists = $IfExists AccessMeta = @{ Role = $Role From c3ff9405df068b5d5797826695eddacf55f96432 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 25 Mar 2024 15:19:45 -0700 Subject: [PATCH 37/84] Remove Write-PodeFileResponse test from Pester 4.x. The test is available for Pester 5.5 --- src/Private/Helpers.ps1 | 33 +++++++++++++++++++++++++++++++++ tests/unit/Responses.Tests.ps1 | 30 ------------------------------ 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/Private/Helpers.ps1 b/src/Private/Helpers.ps1 index 5ed49ee76..ae15606ad 100644 --- a/src/Private/Helpers.ps1 +++ b/src/Private/Helpers.ps1 @@ -1619,6 +1619,39 @@ function Get-PodeCount { return $Object.Count } +<# +.SYNOPSIS + Tests if a given file system path is valid and optionally if it is not a directory. + +.DESCRIPTION + This function tests if the provided file system path is valid. It checks if the path is not null or whitespace, and if the item at the path exists. If the item exists and is not a directory (unless the $FailOnDirectory switch is not used), it returns true. If the path is not valid, it can optionally set a 404 response status code. + +.PARAMETER Path + The file system path to test for validity. + +.PARAMETER NoStatus + A switch to suppress setting the 404 response status code if the path is not valid. + +.PARAMETER FailOnDirectory + A switch to indicate that the function should return false if the path is a directory. + +.EXAMPLE + $isValid = Test-PodePath -Path "C:\temp\file.txt" + if ($isValid) { + # The file exists and is not a directory + } + +.EXAMPLE + $isValid = Test-PodePath -Path "C:\temp\folder" -FailOnDirectory + if (!$isValid) { + # The path is a directory or does not exist + } + +.NOTES + This function is used within the Pode framework to validate file system paths for serving static content. + +#> + function Test-PodePath { param( [Parameter()] diff --git a/tests/unit/Responses.Tests.ps1 b/tests/unit/Responses.Tests.ps1 index 39f4c1ea0..8a0be2992 100644 --- a/tests/unit/Responses.Tests.ps1 +++ b/tests/unit/Responses.Tests.ps1 @@ -330,36 +330,6 @@ Describe 'Write-PodeTextResponse' { } } -Describe 'Write-PodeFileResponse' { - It 'Does nothing when the file does not exist' { - Mock Get-PodeRelativePath { return $Path } - Mock Test-PodePath { return $false } - Write-PodeFileResponse -Path './path' | Out-Null - Assert-MockCalled Test-PodePath -Times 1 -Scope It - } - - Mock Test-PodePath { return $true } - - It 'Loads the contents of a dynamic file' { - Mock Get-PodeRelativePath { return $Path } - Mock Get-PodeFileContentUsingViewEngine { return 'file contents' } - Mock Write-PodeTextResponse { return $Value } - - Write-PodeFileResponse -Path './path/file.pode' | Should Be 'file contents' - - Assert-MockCalled Get-PodeFileContentUsingViewEngine -Times 1 -Scope It - } - - It 'Loads the contents of a static file' { - Mock Get-PodeRelativePath { return $Path } - Mock Get-Content { return 'file contents' } - Mock Write-PodeTextResponse { return $Value } - - Write-PodeFileResponse -Path './path/file.pode' | Should Be 'file contents' - - Assert-MockCalled Get-PodeFileContentUsingViewEngine -Times 1 -Scope It - } -} Describe 'Use-PodePartialView' { $PodeContext = @{ From e3f1476c178ae138f74ab01e13d5ca27e6456f0c Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Mar 2024 09:58:56 -0700 Subject: [PATCH 38/84] Pwsh 7.4.x Workaround for rest https test --- tests/integration/RestApi.Https.Tests.ps1 | 111 ++++++++++++++++------ 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/tests/integration/RestApi.Https.Tests.ps1 b/tests/integration/RestApi.Https.Tests.ps1 index b4cf50f2e..9be0ba85e 100644 --- a/tests/integration/RestApi.Https.Tests.ps1 +++ b/tests/integration/RestApi.Https.Tests.ps1 @@ -4,7 +4,7 @@ param() Describe 'REST API Requests' { BeforeAll { - $splatter = @{} + <# $splatter = @{} if ($PSVersionTable.PSVersion.Major -le 5) { Add-Type @' @@ -23,7 +23,7 @@ Describe 'REST API Requests' { else { $splatter.SkipCertificateCheck = $true } - +#> $Port = 50010 $Endpoint = "https://localhost:$($Port)" @@ -121,68 +121,83 @@ Describe 'REST API Requests' { } Receive-Job -Name 'Pode' | Out-Default - (Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Get @splatter) | Out-Null + # (Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Get @splatter) | Out-Null + curl -s -X DELETE "$($Endpoint)/close" -k Get-Job -Name 'Pode' | Remove-Job -Force } It 'responds back with pong' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/ping" -k) | ConvertFrom-Json $result.Result | Should -Be 'Pong' } It 'responds back with 404 for invalid route' { - { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' + # { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' + $status_code = (curl.exe -s -o /dev/null -w '%{http_code}' "$Endpoint/eek" -k) + $status_code | Should -be 404 } It 'responds back with 405 for incorrect method' { - { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' + #{ Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' + $status_code = (curl.exe -X POST -s -o /dev/null -w '%{http_code}' "$Endpoint/ping" -k) + $status_code | Should -be 405 } It 'responds with simple query parameter' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/data/query?username=rick" -k) | ConvertFrom-Json $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - json' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter + $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: application/json' -d '{"username":"rick"}' -k | ConvertFrom-Json $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - xml' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' @splatter + $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: text/xml' -d 'rick' -k | ConvertFrom-Json $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter forced to json' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter + $result = curl -s -X POST "$($Endpoint)/data/payload-forced-type" -d '{"username":"rick"}' -k | ConvertFrom-Json $result.Username | Should -Be 'rick' } It 'responds with simple route parameter' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/data/param/rick" -k) | ConvertFrom-Json $result.Username | Should -Be 'rick' } It 'responds with simple route parameter long' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/data/param/rick/messages" -k) | ConvertFrom-Json $result.Messages[0] | Should -Be 'Hello, world!' $result.Messages[1] | Should -Be 'Greetings' $result.Messages[2] | Should -Be 'Wubba Lub' } It 'responds ok to remove account' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter + $result = (curl -s -X DELETE "$($Endpoint)/api/rick/remove" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } It 'responds ok to replace account' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter + $result = (curl -s -X PUT "$($Endpoint)/api/rick/replace" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } It 'responds ok to update account' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter + $result = (curl -s -X PATCH "$($Endpoint)/api/rick/update" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } @@ -196,11 +211,21 @@ Describe 'REST API Requests' { $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - $ms.Position = 0 + $compressedData = $ms.ToArray() + $ms.Dispose() + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) # make the request - $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter + $result = curl.exe -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: gzip' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + # $ms.Position = 0 + # $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter $result.Username | Should -Be 'rick' + + # Cleanup the temporary file + Remove-Item -Path $tempFile + } It 'decodes encoded payload parameter - deflate' { @@ -213,11 +238,21 @@ Describe 'REST API Requests' { $gzip = New-Object System.IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - $ms.Position = 0 + $compressedData = $ms.ToArray() + $ms.Dispose() + + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) # make the request - $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter + # $ms.Position = 0 + # $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter + $result = curl.exe -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: deflate' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json $result.Username | Should -Be 'rick' + + # Cleanup the temporary file + Remove-Item -Path $tempFile } It 'decodes encoded payload parameter forced to gzip' { @@ -230,42 +265,60 @@ Describe 'REST API Requests' { $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - $ms.Position = 0 + $compressedData = $ms.ToArray() + $ms.Dispose() + + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) # make the request - $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' @splatter + # $ms.Position = 0 + # $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' @splatter + $result = curl.exe -s -X POST "$Endpoint/encoding/transfer-forced-type" -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json $result.Username | Should -Be 'rick' + + # Cleanup the temporary file + Remove-Item -Path $tempFile } It 'works with any method' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/all" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter + $result = (curl -s -X PUT "$($Endpoint)/all" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter + $result = (curl -s -X PATCH "$($Endpoint)/all" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } It 'route with a wild card' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/api/stuff/hello" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/api/random/hello" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' - $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/api/123/hello" -k) | ConvertFrom-Json $result.Result | Should -Be 'OK' } It 'route importing outer function' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/imported/func/outer" -k) | ConvertFrom-Json $result.Message | Should -Be 'Outer Hello' } It 'route importing outer function' { - $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter + # $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter + $result = (curl -s -X GET "$($Endpoint)/imported/func/inner" -k) | ConvertFrom-Json $result.Message | Should -Be 'Inner Hello' } } \ No newline at end of file From 55390f2d17c0eaa64135eebd014877990bb78951 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Mar 2024 14:37:03 -0700 Subject: [PATCH 39/84] Fix Linux, Mac and Desktop test --- tests/integration/RestApi.Https.Tests.ps1 | 328 +++++++++++++++------- 1 file changed, 224 insertions(+), 104 deletions(-) diff --git a/tests/integration/RestApi.Https.Tests.ps1 b/tests/integration/RestApi.Https.Tests.ps1 index 9be0ba85e..d2d844d6e 100644 --- a/tests/integration/RestApi.Https.Tests.ps1 +++ b/tests/integration/RestApi.Https.Tests.ps1 @@ -4,26 +4,47 @@ param() Describe 'REST API Requests' { BeforeAll { - <# $splatter = @{} - - if ($PSVersionTable.PSVersion.Major -le 5) { + $splatter = @{} + $UseCurl = $true + $version = $PSVersionTable.PSVersion + if ( $version.Major -eq 5) { + # Ignore SSL certificate validation errors Add-Type @' - using System.Net; - using System.Security.Cryptography.X509Certificates; - public class TrustAllCertsPolicy : ICertificatePolicy { - public bool CheckValidationResult( - ServicePoint srvPoint, X509Certificate certificate, - WebRequest request, int certificateProblem) { - return true; - } - } +using System.Net; +using System.Security.Cryptography.X509Certificates; +public class TrustAllCertsPolicy : ICertificatePolicy { +public bool CheckValidationResult( + ServicePoint srvPoint, X509Certificate certificate, + WebRequest request, int certificateProblem) { + return true; +} +} '@ + [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } + $UseCurl = $false + } + elseif ($PSVersionTable.OS -like '*Windows*') { + # OS check passed, now check PowerShell version + # Split version by '.' and compare major and minor version + if ( $version.Major -gt 7 -or ($version.Major -eq 7 -and $version.Minor -ge 4)) { + # Running on Windows with PowerShell Core 7.4 or greater. + $UseCurl = $true + } + else { + $UseCurl = $false + $splatter.SkipCertificateCheck = $true + # Running on Windows but with PowerShell version less than 7.4. + } + } else { + # Not running on Windows." + $UseCurl = $false $splatter.SkipCertificateCheck = $true } -#> + $Port = 50010 $Endpoint = "https://localhost:$($Port)" @@ -115,89 +136,141 @@ Describe 'REST API Requests' { } AfterAll { - $splatter = @{} - if ($PSVersionTable.PSVersion.Major -ge 6) { - $splatter.SkipCertificateCheck = $true - } + # $splatter = @{} + # if ($PSVersionTable.PSVersion.Major -ge 6) { + # $splatter.SkipCertificateCheck = $true + # } Receive-Job -Name 'Pode' | Out-Default - # (Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Get @splatter) | Out-Null - curl -s -X DELETE "$($Endpoint)/close" -k + if ($UseCurl) { + curl -s -X DELETE "$($Endpoint)/close" -k + } + else { + Invoke-RestMethod -Uri "$($Endpoint)/close" -Method Get @splatter | Out-Null + } Get-Job -Name 'Pode' | Remove-Job -Force } It 'responds back with pong' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/ping" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/ping" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Get @splatter + } $result.Result | Should -Be 'Pong' } It 'responds back with 404 for invalid route' { - # { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' - $status_code = (curl.exe -s -o /dev/null -w '%{http_code}' "$Endpoint/eek" -k) - $status_code | Should -be 404 + if ($UseCurl) { + $status_code = (curl -s -o /dev/null -w '%{http_code}' "$Endpoint/eek" -k) + $status_code | Should -be 404 + } + else { + { Invoke-RestMethod -Uri "$($Endpoint)/eek" -Method Get -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*404*' + } } It 'responds back with 405 for incorrect method' { - #{ Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' - $status_code = (curl.exe -X POST -s -o /dev/null -w '%{http_code}' "$Endpoint/ping" -k) - $status_code | Should -be 405 + if ($UseCurl) { + $status_code = (curl -X POST -s -o /dev/null -w '%{http_code}' "$Endpoint/ping" -k) + $status_code | Should -be 405 + } + else { + { Invoke-RestMethod -Uri "$($Endpoint)/ping" -Method Post -ErrorAction Stop @splatter } | Should -Throw -ExpectedMessage '*405*' + } } It 'responds with simple query parameter' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/data/query?username=rick" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/data/query?username=rick" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/query?username=rick" -Method Get @splatter + } $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - json' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter - $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: application/json' -d '{"username":"rick"}' -k | ConvertFrom-Json + if ($UseCurl) { + $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: application/json' -d '{"username":"rick"}' -k | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body '{"username":"rick"}' -ContentType 'application/json' @splatter + } $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter - xml' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' @splatter - $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: text/xml' -d 'rick' -k | ConvertFrom-Json + if ($UseCurl) { + $result = curl -s -X POST "$($Endpoint)/data/payload" -H 'Content-Type: text/xml' -d 'rick' -k | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload" -Method Post -Body 'rick' -ContentType 'text/xml' @splatter + } $result.Username | Should -Be 'rick' } It 'responds with simple payload parameter forced to json' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter - $result = curl -s -X POST "$($Endpoint)/data/payload-forced-type" -d '{"username":"rick"}' -k | ConvertFrom-Json + if ($UseCurl) { + $result = curl -s -X POST "$($Endpoint)/data/payload-forced-type" -d '{"username":"rick"}' -k | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/payload-forced-type" -Method Post -Body '{"username":"rick"}' @splatter + } $result.Username | Should -Be 'rick' } It 'responds with simple route parameter' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/data/param/rick" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/data/param/rick" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick" -Method Get @splatter + } $result.Username | Should -Be 'rick' } It 'responds with simple route parameter long' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/data/param/rick/messages" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/data/param/rick/messages" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/data/param/rick/messages" -Method Get @splatter + } $result.Messages[0] | Should -Be 'Hello, world!' $result.Messages[1] | Should -Be 'Greetings' $result.Messages[2] | Should -Be 'Wubba Lub' } It 'responds ok to remove account' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter - $result = (curl -s -X DELETE "$($Endpoint)/api/rick/remove" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X DELETE "$($Endpoint)/api/rick/remove" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/remove" -Method Delete @splatter + } $result.Result | Should -Be 'OK' } It 'responds ok to replace account' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter - $result = (curl -s -X PUT "$($Endpoint)/api/rick/replace" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X PUT "$($Endpoint)/api/rick/replace" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/replace" -Method Put @splatter + } $result.Result | Should -Be 'OK' } It 'responds ok to update account' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter - $result = (curl -s -X PATCH "$($Endpoint)/api/rick/update" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X PATCH "$($Endpoint)/api/rick/update" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/rick/update" -Method Patch @splatter + } $result.Result | Should -Be 'OK' } @@ -211,20 +284,27 @@ Describe 'REST API Requests' { $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - $compressedData = $ms.ToArray() - $ms.Dispose() - - # Save the compressed data to a temporary file - $tempFile = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempFile, $compressedData) - # make the request - $result = curl.exe -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: gzip' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json - # $ms.Position = 0 - # $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter - $result.Username | Should -Be 'rick' - # Cleanup the temporary file - Remove-Item -Path $tempFile + if ($UseCurl) { + $compressedData = $ms.ToArray() + $ms.Dispose() + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) + # make the request + $result = curl -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: gzip' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + + # Cleanup the temporary file + Remove-Item -Path $tempFile + } + else { + # make the request + $ms.Position = 0 + $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'gzip' } -ContentType 'application/json' @splatter + $ms.Dispose() + } + + $result.Username | Should -Be 'rick' } @@ -238,21 +318,28 @@ Describe 'REST API Requests' { $gzip = New-Object System.IO.Compression.DeflateStream($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() - $compressedData = $ms.ToArray() - $ms.Dispose() + if ($UseCurl) { + $compressedData = $ms.ToArray() + $ms.Dispose() - # Save the compressed data to a temporary file - $tempFile = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempFile, $compressedData) + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) - # make the request - # $ms.Position = 0 - # $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter - $result = curl.exe -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: deflate' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json - $result.Username | Should -Be 'rick' + # make the request + $result = curl -s -X POST "$Endpoint/encoding/transfer" -H 'Transfer-Encoding: deflate' -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + + # Cleanup the temporary file + Remove-Item -Path $tempFile + } + else { + # make the request + $ms.Position = 0 + $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer" -Method Post -Body $ms.ToArray() -Headers @{ 'Transfer-Encoding' = 'deflate' } -ContentType 'application/json' @splatter + $ms.Dispose() + } - # Cleanup the temporary file - Remove-Item -Path $tempFile + $result.Username | Should -Be 'rick' } It 'decodes encoded payload parameter forced to gzip' { @@ -265,60 +352,93 @@ Describe 'REST API Requests' { $gzip = New-Object System.IO.Compression.GZipStream($ms, [IO.Compression.CompressionMode]::Compress, $true) $gzip.Write($bytes, 0, $bytes.Length) $gzip.Close() + if ($UseCurl) { - $compressedData = $ms.ToArray() - $ms.Dispose() + $compressedData = $ms.ToArray() + $ms.Dispose() - # Save the compressed data to a temporary file - $tempFile = [System.IO.Path]::GetTempFileName() - [System.IO.File]::WriteAllBytes($tempFile, $compressedData) - # make the request - # $ms.Position = 0 - # $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' @splatter - $result = curl.exe -s -X POST "$Endpoint/encoding/transfer-forced-type" -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json - $result.Username | Should -Be 'rick' + # Save the compressed data to a temporary file + $tempFile = [System.IO.Path]::GetTempFileName() + [System.IO.File]::WriteAllBytes($tempFile, $compressedData) + # make the request + $result = curl -s -X POST "$Endpoint/encoding/transfer-forced-type" -H 'Content-Type: application/json' --data-binary "@$tempFile" -k | ConvertFrom-Json + + # Cleanup the temporary file + Remove-Item -Path $tempFile + } + else { + # make the request + $ms.Position = 0 + $result = Invoke-RestMethod -Uri "$($Endpoint)/encoding/transfer-forced-type" -Method Post -Body $ms.ToArray() -ContentType 'application/json' @splatter + $ms.Dispose() + } - # Cleanup the temporary file - Remove-Item -Path $tempFile + $result.Username | Should -Be 'rick' } It 'works with any method' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/all" -k) | ConvertFrom-Json - $result.Result | Should -Be 'OK' + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/all" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' - # $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter - $result = (curl -s -X PUT "$($Endpoint)/all" -k) | ConvertFrom-Json - $result.Result | Should -Be 'OK' + $result = (curl -s -X PUT "$($Endpoint)/all" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' - # $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter - $result = (curl -s -X PATCH "$($Endpoint)/all" -k) | ConvertFrom-Json - $result.Result | Should -Be 'OK' + $result = (curl -s -X PATCH "$($Endpoint)/all" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Get @splatter + $result.Result | Should -Be 'OK' + + $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Put @splatter + $result.Result | Should -Be 'OK' + + $result = Invoke-RestMethod -Uri "$($Endpoint)/all" -Method Patch @splatter + $result.Result | Should -Be 'OK' + } } It 'route with a wild card' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/api/stuff/hello" -k) | ConvertFrom-Json - $result.Result | Should -Be 'OK' + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/api/stuff/hello" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' - # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/api/random/hello" -k) | ConvertFrom-Json - $result.Result | Should -Be 'OK' + $result = (curl -s -X GET "$($Endpoint)/api/random/hello" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' - # $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/api/123/hello" -k) | ConvertFrom-Json - $result.Result | Should -Be 'OK' + $result = (curl -s -X GET "$($Endpoint)/api/123/hello" -k) | ConvertFrom-Json + $result.Result | Should -Be 'OK' + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/stuff/hello" -Method Get @splatter + $result.Result | Should -Be 'OK' + + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/random/hello" -Method Get @splatter + $result.Result | Should -Be 'OK' + + $result = Invoke-RestMethod -Uri "$($Endpoint)/api/123/hello" -Method Get @splatter + $result.Result | Should -Be 'OK' + } } It 'route importing outer function' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/imported/func/outer" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/imported/func/outer" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/outer" -Method Get @splatter + } $result.Message | Should -Be 'Outer Hello' } It 'route importing outer function' { - # $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter - $result = (curl -s -X GET "$($Endpoint)/imported/func/inner" -k) | ConvertFrom-Json + if ($UseCurl) { + $result = (curl -s -X GET "$($Endpoint)/imported/func/inner" -k) | ConvertFrom-Json + } + else { + $result = Invoke-RestMethod -Uri "$($Endpoint)/imported/func/inner" -Method Get @splatter + } $result.Message | Should -Be 'Inner Hello' } } \ No newline at end of file From a14faf12d36167cf148f86b95906fdcf5aac7aeb Mon Sep 17 00:00:00 2001 From: mdaneri Date: Tue, 26 Mar 2024 15:19:51 -0700 Subject: [PATCH 40/84] cleanup --- tests/integration/RestApi.Https.Tests.ps1 | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/integration/RestApi.Https.Tests.ps1 b/tests/integration/RestApi.Https.Tests.ps1 index d2d844d6e..1ff67da8d 100644 --- a/tests/integration/RestApi.Https.Tests.ps1 +++ b/tests/integration/RestApi.Https.Tests.ps1 @@ -22,7 +22,6 @@ public bool CheckValidationResult( '@ [System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy - [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } $UseCurl = $false } elseif ($PSVersionTable.OS -like '*Windows*') { @@ -136,11 +135,6 @@ public bool CheckValidationResult( } AfterAll { - # $splatter = @{} - # if ($PSVersionTable.PSVersion.Major -ge 6) { - # $splatter.SkipCertificateCheck = $true - # } - Receive-Job -Name 'Pode' | Out-Default if ($UseCurl) { curl -s -X DELETE "$($Endpoint)/close" -k From 9ab6286c622a42e23e68a08bbb04f0c1cb1b82ea Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Mar 2024 16:04:23 -0700 Subject: [PATCH 41/84] Hide the get-item exception 'referred to an item that was outside the base' --- src/Private/Responses.ps1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Private/Responses.ps1 b/src/Private/Responses.ps1 index 2e0616ca9..26835f10e 100644 --- a/src/Private/Responses.ps1 +++ b/src/Private/Responses.ps1 @@ -161,7 +161,7 @@ function Write-PodeFileResponseInternal { ) # Attempt to retrieve information about the path - $pathInfo = Get-Item -Path $Path -force -ErrorAction Continue + $pathInfo = Get-Item -Path $Path -force -ErrorAction SilentlyContinue # Check if the path exists if ($null -eq $pathInfo) { @@ -433,7 +433,7 @@ function Write-PodeAttachmentResponseInternal { $Path = Get-PodeRelativePath -Path $Path -JoinRoot # Attempt to retrieve information about the path - $pathInfo = Get-Item -Path $Path -force -ErrorAction Continue + $pathInfo = Get-Item -Path $Path -force -ErrorAction SilentlyContinue # Check if the path exists if ($null -eq $pathInfo) { #if not exist try with to find with public Route if exist @@ -442,7 +442,7 @@ function Write-PodeAttachmentResponseInternal { # only attach files from public/static-route directories when path is relative $Path = Get-PodeRelativePath -Path $Path -JoinRoot # Attempt to retrieve information about the path - $pathInfo = Get-Item -Path $Path -ErrorAction Continue + $pathInfo = Get-Item -Path $Path -ErrorAction SilentlyContinue } if ($null -eq $pathInfo) { # If not, set the response status to 404 Not Found From 02861cf68d13b8bfe881029e2f4612ffc56c869d Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Mar 2024 19:00:28 -0700 Subject: [PATCH 42/84] Update pode.build.ps1 --- pode.build.ps1 | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index 82ea8aacf..88d7c02de 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -322,7 +322,7 @@ Task Pack Build, { #> # Synopsis: Run the tests -Task Test Build, TestDeps, { +Task TestNoBuild TestDeps, { $p = (Get-Command Invoke-Pester) if ($null -eq $p -or $p.Version -ine $Versions.Pester) { Remove-Module Pester -Force -ErrorAction Ignore @@ -350,6 +350,10 @@ Task Test Build, TestDeps, { } }, PushCodeCoverage, CheckFailedTests +# Synopsis: Run tests after a build +Task Test Build, TestNoBuild + + # Synopsis: Check if any of the tests failed Task CheckFailedTests { if ($TestStatus.FailedCount -gt 0) { @@ -431,32 +435,45 @@ Task DocsBuild DocsDeps, DocsHelpBuild, { mkdocs build } - +# Synopsis: Clean the build enviroment Task Clean { $path = './deliverable' if (Test-Path -Path $path -PathType Container) { - Remove-Item -Path $path -Recurse -Force | Out-Null + Write-Host "Removing ./deliverable folder" + Remove-Item -Path $path -Recurse -Force -Verbose | Out-Null } $path = './pkg' - if ((Test-Path -Path $path -PathType Container )) { - Remove-Item -Path $path -Recurse -Force | Out-Null + Write-Host "Removing ./pkg folder" + Remove-Item -Path $path -Recurse -Force -Verbose | Out-Null } if ((Test-Path -Path .\packers\choco\tools\ChocolateyInstall.ps1 -PathType Leaf )) { + Write-Host "Removing .\packers\choco\tools\ChocolateyInstall.ps1" Remove-Item -Path .\packers\choco\tools\ChocolateyInstall.ps1 } if ((Test-Path -Path .\packers\choco\pode.nuspec -PathType Leaf )) { + Write-Host "Removing .\packers\choco\pode.nuspec" Remove-Item -Path .\packers\choco\pode.nuspec } - Write-Host "$path Cleanup done" + Write-Host "Cleanup done" } -Task Install-Module { - $path = './pkg' +# Synopsis: Clean the build enviroment including libs +Task CleanAll { + $path = './src/Libs' + if (Test-Path -Path $path -PathType Container) { + Write-Host "Removing ./src/Libs contents" + Remove-Item -Path $path -Recurse -Force | Out-Null + } +}, Clean + +# Synopsis: Install Pode Module locally +Task Install-Module { + $path = './pkg' if ($Version) { if (! (Test-Path $path)) { @@ -497,7 +514,7 @@ Task Install-Module { } - +# Synopsis: Remove the Pode Module from the local registry Task Remove-Module { if ($Version) { From 23d0d52b807a0c1535f9983c9b972ae6227b26a3 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Mar 2024 19:30:29 -0700 Subject: [PATCH 43/84] PSScriptAnalyzerSettings --- .vscode/settings.json | 1 + PSScriptAnalyzerSettings.psd1 | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 PSScriptAnalyzerSettings.psd1 diff --git a/.vscode/settings.json b/.vscode/settings.json index fb2be60f2..559d3ecf2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,6 +20,7 @@ "powershell.codeFormatting.whitespaceBeforeOpenParen": true, "powershell.codeFormatting.whitespaceBetweenParameters": false, "powershell.codeFormatting.whitespaceInsideBrace": true, + "powershell.scriptAnalysis.settingsPath": "PSScriptAnalyzerSettings.psd1", "files.trimTrailingWhitespace": true, "files.associations": { "*.pode": "html" diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 000000000..db51e2df4 --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,6 @@ +# PSScriptAnalyzerSettings.psd1 +@{ + Severity = @('Error', 'Warning', 'Information') + ExcludeRules = @('PSAvoidUsingCmdletAliases' ,'PSAvoidUsingPlainTextForPassword','PSAvoidUsingWriteHost','PSAvoidUsingInvokeExpression','PSUseShouldProcessForStateChangingFunctions', + 'PSAvoidUsingUsernameAndPasswordParams','PSUseProcessBlockForPipelineCommand','PSAvoidUsingConvertToSecureStringWithPlainText') +} \ No newline at end of file From 9120aaa45efe2b971a6ceaa7725be9c6fd27ae5b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Wed, 27 Mar 2024 19:34:25 -0700 Subject: [PATCH 44/84] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff02d022d..bf2a4a28b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,7 +25,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v1.9.0 with: - dotnet-version: 7.0.x + dotnet-version: 8.0.x - name: Install Invoke-Build shell: pwsh From bb1a129e67432ef5c22648025e26f0e72d9e378f Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 29 Mar 2024 14:35:12 -0700 Subject: [PATCH 45/84] Applied Matthew suggestion --- .gitignore | 6 +++--- docs/Getting-Started/build.md | 12 ++++++------ pode.build.ps1 | 29 +++++++++++++++++------------ 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 3d08e5422..4375121dd 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ examples/issues/ pkg/ .vs/ - # Code Runner tempCodeRunnerFile.ps1 @@ -189,7 +188,6 @@ ClientBin/ *.publishsettings node_modules/ - # RIA/Silverlight projects Generated_Code/ @@ -258,4 +256,6 @@ $RECYCLE.BIN/ *.msp # Windows shortcuts -*.lnk \ No newline at end of file +*.lnk +packers/choco/pode.nuspec +packers/choco/tools/ChocolateyInstall.ps1 diff --git a/docs/Getting-Started/build.md b/docs/Getting-Started/build.md index b335cfd9c..0d7fd38da 100644 --- a/docs/Getting-Started/build.md +++ b/docs/Getting-Started/build.md @@ -1,5 +1,5 @@ -# Build Pode locally +# Build Pode To build and use the code checked out on your machine, follow these steps : @@ -90,14 +90,14 @@ To build and use the code checked out on your machine, follow these steps : ``` -3. Install InvokeBuild Module +2. Install InvokeBuild Module ```powershell Install-Module InvokeBuild -Scope CurrentUser ``` -4. Test +3. Test To run the unit tests, run the following command from the root of the repository (this will build Pode and, if needed, auto-install Pester/.NET): @@ -105,7 +105,7 @@ To build and use the code checked out on your machine, follow these steps : Invoke-Build Test ``` -5. Build +4. Build To just build Pode, before running any examples, run the following: @@ -113,7 +113,7 @@ To build and use the code checked out on your machine, follow these steps : Invoke-Build Build ``` -6. Packaging +5. Packaging To create a Pode package. Please note that docker has to be present to create the containers. @@ -121,7 +121,7 @@ To build and use the code checked out on your machine, follow these steps : Invoke-Build Pack ``` -7. Install locally +6. Install locally To install Pode from the repository, run the following: diff --git a/pode.build.ps1 b/pode.build.ps1 index 88d7c02de..9183c03d9 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -90,11 +90,10 @@ function Install-PodeBuildModule($name) { Install-Module -Name "$($name)" -Scope CurrentUser -RequiredVersion "$($Versions[$name])" -Force -SkipPublisherCheck } -function Invoke-PodeBuildDotnetBuild($target,$Version) { +function Invoke-PodeBuildDotnetBuild($target ) { # Retrieve the highest installed SDK version - $highestSdkVersion = dotnet --list-sdks | Select-Object -Last 1 | ForEach-Object { $_.Split(' ')[0] } - $majorVersion = [int]$highestSdkVersion.Split('.')[0] + $majorVersion = ([version](dotnet --version)).Major # Determine if the target framework is compatible $isCompatible = $False @@ -345,7 +344,6 @@ Task TestNoBuild TestDeps, { $Script:TestStatus = Invoke-Pester -Configuration $configuration } else { - $configuration.Output.Verbosity = 'Detailed' $Script:TestStatus = Invoke-Pester -Configuration $configuration } }, PushCodeCoverage, CheckFailedTests @@ -436,17 +434,24 @@ Task DocsBuild DocsDeps, DocsHelpBuild, { } # Synopsis: Clean the build enviroment -Task Clean { +Task Clean CleanPkg,CleanDeliverable,CleanLibs + +# Synopsis: Clean the Deliverable folder +Task CleanDeliverable { $path = './deliverable' if (Test-Path -Path $path -PathType Container) { Write-Host "Removing ./deliverable folder" - Remove-Item -Path $path -Recurse -Force -Verbose | Out-Null + Remove-Item -Path $path -Recurse -Force | Out-Null } + Write-Host "Cleanup $path done" +} +# Synopsis: Clean the pkg directory +Task CleanPkg { $path = './pkg' if ((Test-Path -Path $path -PathType Container )) { Write-Host "Removing ./pkg folder" - Remove-Item -Path $path -Recurse -Force -Verbose | Out-Null + Remove-Item -Path $path -Recurse -Force | Out-Null } if ((Test-Path -Path .\packers\choco\tools\ChocolateyInstall.ps1 -PathType Leaf )) { @@ -457,18 +462,18 @@ Task Clean { Write-Host "Removing .\packers\choco\pode.nuspec" Remove-Item -Path .\packers\choco\pode.nuspec } - Write-Host "Cleanup done" + Write-Host "Cleanup $path done" } - -# Synopsis: Clean the build enviroment including libs -Task CleanAll { +# Synopsis: Clean the libs folder +Task CleanLibs { $path = './src/Libs' if (Test-Path -Path $path -PathType Container) { Write-Host "Removing ./src/Libs contents" Remove-Item -Path $path -Recurse -Force | Out-Null } -}, Clean + Write-Host "Cleanup $path done" +} # Synopsis: Install Pode Module locally From 16402ba242618181e708bc76433bdf2c1034563b Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 29 Mar 2024 14:58:11 -0700 Subject: [PATCH 46/84] Update PSScriptAnalyzerSettings.psd1 --- PSScriptAnalyzerSettings.psd1 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 index db51e2df4..2141ac49d 100644 --- a/PSScriptAnalyzerSettings.psd1 +++ b/PSScriptAnalyzerSettings.psd1 @@ -1,6 +1,15 @@ # PSScriptAnalyzerSettings.psd1 @{ Severity = @('Error', 'Warning', 'Information') + + Rules = @{ + PSReviewUnusedParameter = @{ + CommandsToTraverse = @( + 'Where-Object','Remove-PodeRoute' + ) + } + } ExcludeRules = @('PSAvoidUsingCmdletAliases' ,'PSAvoidUsingPlainTextForPassword','PSAvoidUsingWriteHost','PSAvoidUsingInvokeExpression','PSUseShouldProcessForStateChangingFunctions', - 'PSAvoidUsingUsernameAndPasswordParams','PSUseProcessBlockForPipelineCommand','PSAvoidUsingConvertToSecureStringWithPlainText') + 'PSAvoidUsingUsernameAndPasswordParams','PSUseProcessBlockForPipelineCommand','PSAvoidUsingConvertToSecureStringWithPlainText','PSUseSingularNouns','PSReviewUnusedParameter' ) + } \ No newline at end of file From 1bddadfd57c6a56768d346914ee6a4a9397e57c5 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 29 Mar 2024 16:16:23 -0700 Subject: [PATCH 47/84] Update Routes.ps1 --- src/Public/Routes.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Public/Routes.ps1 b/src/Public/Routes.ps1 index 9a439cdb8..8b0f3e920 100644 --- a/src/Public/Routes.ps1 +++ b/src/Public/Routes.ps1 @@ -2166,10 +2166,10 @@ function Add-PodePage { # invoke the function (optional splat data) if (Test-PodeIsEmpty $data) { - $result = & $script + $result = Invoke-PodeScriptBlock -ScriptBlock $script -Return } else { - $result = & $script @data + $result = Invoke-PodeScriptBlock -ScriptBlock $script -Arguments $data -Return } # if we have a result, convert it to html From 8bf27390cfb54e71abfe27154140c0c74e565001 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Fri, 29 Mar 2024 16:22:23 -0700 Subject: [PATCH 48/84] Update pode.build.ps1 --- pode.build.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index 9183c03d9..8b8b9f0c2 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -225,10 +225,10 @@ Task Build BuildDeps, { Push-Location ./src/Listener try { - Invoke-PodeBuildDotnetBuild -target 'netstandard2.0' -Version $Version - Invoke-PodeBuildDotnetBuild -target 'net6.0' -Version $Version - Invoke-PodeBuildDotnetBuild -target 'net7.0' -Version $Version - Invoke-PodeBuildDotnetBuild -target 'net8.0' -Version $Version + Invoke-PodeBuildDotnetBuild -target 'netstandard2.0' + Invoke-PodeBuildDotnetBuild -target 'net6.0' + Invoke-PodeBuildDotnetBuild -target 'net7.0' + Invoke-PodeBuildDotnetBuild -target 'net8.0' } finally { Pop-Location From 6bc890822c97bbdb1a1e5b466b9701132b71b896 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Mar 2024 09:17:41 -0700 Subject: [PATCH 49/84] Applied Matthew suggestions --- .gitignore | 1 + packers/choco/pode_template.nuspec | 5 +++-- pode.build.ps1 | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 4375121dd..2a22b29a2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ examples/state.json examples/issue-* examples/issues/ pkg/ +deliverable/ .vs/ # Code Runner diff --git a/packers/choco/pode_template.nuspec b/packers/choco/pode_template.nuspec index 6539329ef..2a9093c65 100644 --- a/packers/choco/pode_template.nuspec +++ b/packers/choco/pode_template.nuspec @@ -52,9 +52,10 @@ Pode is a Cross-Platform framework for creating web servers to host REST APIs an https://raw.githubusercontent.com/Badgerati/Pode/master/images/icon.png https://github.com/Badgerati/Pode/releases - - + + + \ No newline at end of file diff --git a/pode.build.ps1 b/pode.build.ps1 index 8b8b9f0c2..e2a44b618 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -254,7 +254,7 @@ Task Compress StampVersion, { } # create the pkg dir New-Item -Path $path -ItemType Directory -Force | Out-Null - Compress-Archive -Path './pkg' -DestinationPath "$path/$Version-Binaries.zip" + Compress-Archive -Path './pkg/*' -DestinationPath "$path/$Version-Binaries.zip" }, PrintChecksum # Synopsis: Creates a Chocolately package of the Module From 9c05b13006c10119e1aa24c3f21ced8f12a1e849 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Mar 2024 09:45:11 -0700 Subject: [PATCH 50/84] Update StaticContent.md --- .../Routes/Utilities/StaticContent.md | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/Tutorials/Routes/Utilities/StaticContent.md b/docs/Tutorials/Routes/Utilities/StaticContent.md index 1aafda415..69b83e1bb 100644 --- a/docs/Tutorials/Routes/Utilities/StaticContent.md +++ b/docs/Tutorials/Routes/Utilities/StaticContent.md @@ -6,7 +6,8 @@ Caching is supported on static content. ## Public Directory -You can place static files within the `/public` directory at the root of your server, which serves as the default location for static content. However, if you need to relocate this directory, you can do so programmatically using the `Set-PodeStaticFolder` function within your server script, or specify a different location in the `server.psd1` configuration file under the `Server.DefaultFolders` property. When a request is made for a file, Pode will automatically check this designated static directory first, and if the file is found, it will be returned to the requester. +You can place static files within the `/public` directory at the root of your server, which serves as the default location for static content. When a request is made for a file, Pode will automatically check this designated static directory first, and if the file is found, it will be returned to the requester. + For example, if you have a `logic.js` at `/public/scripts/logic.js`. The the following request would return the file's content: @@ -20,6 +21,37 @@ Or, you can reference the file in a view like: ``` +### How to change the Default Folders + +Usually, the Default Folders are located under the RootPath specified by `Start-PodeServer -RootPath `. +But if you need to relocate this directory, you can do so programmatically using the `Set-PodeStaticFolder` function within your server script or specify a different location in the `server.psd1` configuration file under the `Server.DefaultFolders` property. When a file request is made, Pode will automatically check this designated static directory first, and if the file is found, it will be returned to the requester. + +Here an example: + +1. Using `Set-PodeStaticFolder` + +```powershell +Set-PodeDefaultFolder -Type 'Public' -Path 'c:\custom\public' +Set-PodeDefaultFolder -Type 'Views' -Path 'd:\shared\views' +Set-PodeDefaultFolder -Type 'Errors' -Path 'e:\logs\errors' +``` + +2. Using `server.psd1` configuration file + +```powershell +@{ + # For more information https://badgerati.github.io/Pode/Tutorials/Configuration/ + Server = @{ + # Any othe properties you need in your application + DefaultFolders = @{ + Public = 'c:\custom\public' + Views = 'd:\shared\views' + Errors = 'e:\logs\errors' + } + } +} +``` + ## Static Routes The following is an example of using the [`Add-PodeStaticRoute`](../../../../Functions/Routes/Add-PodeStaticRoute) function to define a route to some static content directory; this tells Pode where to get static files from for certain routes. This example will define a static route for `/assets`, and will point the route at the internal directory path of `./content/assets`: From bbd7a2353a275348f43244cbdfdc490dbef4f458 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Mar 2024 11:40:22 -0700 Subject: [PATCH 51/84] Update pode.build.ps1 --- pode.build.ps1 | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pode.build.ps1 b/pode.build.ps1 index e2a44b618..930714a38 100644 --- a/pode.build.ps1 +++ b/pode.build.ps1 @@ -434,7 +434,7 @@ Task DocsBuild DocsDeps, DocsHelpBuild, { } # Synopsis: Clean the build enviroment -Task Clean CleanPkg,CleanDeliverable,CleanLibs +Task Clean CleanPkg,CleanDeliverable,CleanLibs,CleanListener # Synopsis: Clean the Deliverable folder Task CleanDeliverable { @@ -469,12 +469,21 @@ Task CleanPkg { Task CleanLibs { $path = './src/Libs' if (Test-Path -Path $path -PathType Container) { - Write-Host "Removing ./src/Libs contents" + Write-Host "Removing $path contents" Remove-Item -Path $path -Recurse -Force | Out-Null } Write-Host "Cleanup $path done" } +# Synopsis: Clean the Listener folder +Task CleanListener { + $path = './src/Listener/bin' + if (Test-Path -Path $path -PathType Container) { + Write-Host "Removing $path contents" + Remove-Item -Path $path -Recurse -Force | Out-Null + } + Write-Host "Cleanup $path done" +} # Synopsis: Install Pode Module locally Task Install-Module { From 6d5ecb28f2bf211c51637f5f4d1298d5911a1373 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Mar 2024 18:04:45 -0700 Subject: [PATCH 52/84] Added multiple tests --- .github/workflows/ci-pwsh7_2.yml | 56 +++++++++++++++++++ .github/workflows/ci-pwsh7_3.yml | 39 +++++++------ .github/workflows/ci-pwsh7_4.yml | 53 ------------------ .github/workflows/ci-pwsh7_5.yml | 53 ------------------ .github/workflows/{ci.yml => ci-pwsh_lts.yml} | 5 ++ .github/workflows/ci-pwsh_preview.yml | 56 +++++++++++++++++++ 6 files changed, 138 insertions(+), 124 deletions(-) create mode 100644 .github/workflows/ci-pwsh7_2.yml delete mode 100644 .github/workflows/ci-pwsh7_4.yml delete mode 100644 .github/workflows/ci-pwsh7_5.yml rename .github/workflows/{ci.yml => ci-pwsh_lts.yml} (90%) create mode 100644 .github/workflows/ci-pwsh_preview.yml diff --git a/.github/workflows/ci-pwsh7_2.yml b/.github/workflows/ci-pwsh7_2.yml new file mode 100644 index 000000000..3b37308bc --- /dev/null +++ b/.github/workflows/ci-pwsh7_2.yml @@ -0,0 +1,56 @@ +name: Pode CI - pwsh + +on: + push: + branches: + - '*' + - '!gh-pages' + pull_request: + branches: + - '*' + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Powershell + uses: bjompen/UpdatePWSHAction@v1.0.0 + with: + FixedVersion: '7.2.18' + + - name: Check PowerShell version + shell: pwsh + run: | + $PSVersionTable.PSVersion + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Invoke-Build + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force + + - name: Run Pester Tests + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build Test + + - name: Test docker builds + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build DockerPack -Version '0.0.0' \ No newline at end of file diff --git a/.github/workflows/ci-pwsh7_3.yml b/.github/workflows/ci-pwsh7_3.yml index 8cb94de60..9135bf90e 100644 --- a/.github/workflows/ci-pwsh7_3.yml +++ b/.github/workflows/ci-pwsh7_3.yml @@ -1,4 +1,4 @@ -name: Pode CI - pwsh 7.3 on windows-latest +name: Pode CI - pwsh on: push: @@ -12,14 +12,26 @@ on: jobs: build: - runs-on: windows-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] steps: - uses: actions/checkout@v4 + - name: Setup Powershell + uses: bjompen/UpdatePWSHAction@v1.0.0 + with: + FixedVersion: '7.3.11' + + - name: Check PowerShell version + shell: pwsh + run: | + $PSVersionTable.PSVersion + - name: Setup .NET uses: actions/setup-dotnet@v4 with: @@ -28,26 +40,17 @@ jobs: - name: Install Invoke-Build shell: pwsh run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - # For Windows - - name: Download and extract PowerShell 7.3 - run: | - Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.3.11/PowerShell-7.3.11-win-x64.zip" -OutFile "PowerShell-7.3.x-win-x64.zip" - Expand-Archive -LiteralPath "PowerShell-7.3.x-win-x64.zip" -DestinationPath "C:\PowerShell-7.3" - - - name: Check PowerShell version - run: | - C:\PowerShell-7.3\pwsh.exe -version - - name: Run Pester Tests + shell: pwsh run: | - C:\PowerShell-7.3\pwsh.exe -Command { - Invoke-Build Test - } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build Test - name: Test docker builds + shell: pwsh run: | - C:\PowerShell-7.3\pwsh.exe -Command { - Invoke-Build DockerPack -Version '0.0.0' - } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build DockerPack -Version '0.0.0' \ No newline at end of file diff --git a/.github/workflows/ci-pwsh7_4.yml b/.github/workflows/ci-pwsh7_4.yml deleted file mode 100644 index 80d0c1b4a..000000000 --- a/.github/workflows/ci-pwsh7_4.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Pode CI - pwsh 7.4 on windows-latest - -on: - push: - branches: - - '*' - - '!gh-pages' - pull_request: - branches: - - '*' - -jobs: - build: - - runs-on: windows-latest - - strategy: - fail-fast: false - - steps: - - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Install Invoke-Build - shell: pwsh - run: | - Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - - # For Windows - - name: Download and extract PowerShell 7.4 - run: | - Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.4.1/PowerShell-7.4.1-win-x64.zip" -OutFile "PowerShell-7.4.x-win-x64.zip" - Expand-Archive -LiteralPath "PowerShell-7.4.x-win-x64.zip" -DestinationPath "C:\PowerShell-7.4" - - - name: Check PowerShell version - run: | - C:\PowerShell-7.4\pwsh.exe -version - - - name: Run Pester Tests - run: | - C:\PowerShell-7.4\pwsh.exe -Command { - Invoke-Build Test - } - - - name: Test docker builds - run: | - C:\PowerShell-7.4\pwsh.exe -Command { - Invoke-Build DockerPack -Version '0.0.0' - } diff --git a/.github/workflows/ci-pwsh7_5.yml b/.github/workflows/ci-pwsh7_5.yml deleted file mode 100644 index 63757c133..000000000 --- a/.github/workflows/ci-pwsh7_5.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Pode CI - pwsh 7.5 on windows-latest - -on: - push: - branches: - - '*' - - '!gh-pages' - pull_request: - branches: - - '*' - -jobs: - build: - - runs-on: windows-latest - - strategy: - fail-fast: false - - steps: - - uses: actions/checkout@v4 - - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Install Invoke-Build - shell: pwsh - run: | - Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - - # For Windows - - name: Download and extract PowerShell 7.5 - run: | - Invoke-WebRequest -Uri "https://github.com/PowerShell/PowerShell/releases/download/v7.5.0-preview.2/PowerShell-7.5.0-preview.2-win-x64.zip" -OutFile "PowerShell-7.5.x-win-x64.zip" - Expand-Archive -LiteralPath "PowerShell-7.5.x-win-x64.zip" -DestinationPath "C:\PowerShell-7.5" - - - name: Check PowerShell version - run: | - C:\PowerShell-7.5\pwsh.exe -version - - - name: Run Pester Tests - run: | - C:\PowerShell-7.5\pwsh.exe -Command { - Invoke-Build Test - } - - - name: Test docker builds - run: | - C:\PowerShell-7.5\pwsh.exe -Command { - Invoke-Build DockerPack -Version '0.0.0' - } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci-pwsh_lts.yml similarity index 90% rename from .github/workflows/ci.yml rename to .github/workflows/ci-pwsh_lts.yml index 35ac90284..999582dff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -22,6 +22,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup Powershell + uses: bjompen/UpdatePWSHAction@v1.0.0 + with: + ReleaseVersion: 'lts' + - name: Check PowerShell version shell: pwsh run: | diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml new file mode 100644 index 000000000..b1ac22ea1 --- /dev/null +++ b/.github/workflows/ci-pwsh_preview.yml @@ -0,0 +1,56 @@ +name: Pode CI - pwsh + +on: + push: + branches: + - '*' + - '!gh-pages' + pull_request: + branches: + - '*' + +jobs: + build: + + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Powershell + uses: bjompen/UpdatePWSHAction@v1.0.0 + with: + ReleaseVersion: 'Preview' + + - name: Check PowerShell version + shell: pwsh + run: | + $PSVersionTable.PSVersion + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Install Invoke-Build + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force + + - name: Run Pester Tests + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build Test + + - name: Test docker builds + shell: pwsh + run: | + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Invoke-Build DockerPack -Version '0.0.0' \ No newline at end of file From 0d7666767411c98a58d27de8c0ae9625f76bf4f5 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Mar 2024 18:11:59 -0700 Subject: [PATCH 53/84] fixes --- .github/workflows/ci-pwsh7_2.yml | 7 ++----- .github/workflows/ci-pwsh7_3.yml | 5 +---- .github/workflows/ci-pwsh_lts.yml | 7 ++----- .github/workflows/ci-pwsh_preview.yml | 6 ++---- 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-pwsh7_2.yml b/.github/workflows/ci-pwsh7_2.yml index 3b37308bc..644de40d4 100644 --- a/.github/workflows/ci-pwsh7_2.yml +++ b/.github/workflows/ci-pwsh7_2.yml @@ -1,4 +1,4 @@ -name: Pode CI - pwsh +name: Pode CI - pwsh 7.2 on: push: @@ -39,18 +39,15 @@ jobs: - name: Install Invoke-Build shell: pwsh - run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + run: | Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Build Test - name: Test docker builds shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Build DockerPack -Version '0.0.0' \ No newline at end of file diff --git a/.github/workflows/ci-pwsh7_3.yml b/.github/workflows/ci-pwsh7_3.yml index 9135bf90e..82e03e4ee 100644 --- a/.github/workflows/ci-pwsh7_3.yml +++ b/.github/workflows/ci-pwsh7_3.yml @@ -1,4 +1,4 @@ -name: Pode CI - pwsh +name: Pode CI - pwsh 7.3 on: push: @@ -40,17 +40,14 @@ jobs: - name: Install Invoke-Build shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Build Test - name: Test docker builds shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Build DockerPack -Version '0.0.0' \ No newline at end of file diff --git a/.github/workflows/ci-pwsh_lts.yml b/.github/workflows/ci-pwsh_lts.yml index 999582dff..ee811c145 100644 --- a/.github/workflows/ci-pwsh_lts.yml +++ b/.github/workflows/ci-pwsh_lts.yml @@ -1,4 +1,4 @@ -name: Pode CI - pwsh +name: Pode CI - pwsh lts on: push: @@ -40,17 +40,14 @@ jobs: - name: Install Invoke-Build shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Build Test - name: Test docker builds shell: pwsh - run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + run: | Invoke-Build DockerPack -Version '0.0.0' \ No newline at end of file diff --git a/.github/workflows/ci-pwsh_preview.yml b/.github/workflows/ci-pwsh_preview.yml index b1ac22ea1..110e3d92f 100644 --- a/.github/workflows/ci-pwsh_preview.yml +++ b/.github/workflows/ci-pwsh_preview.yml @@ -1,4 +1,4 @@ -name: Pode CI - pwsh +name: Pode CI - pwsh preview on: push: @@ -40,13 +40,11 @@ jobs: - name: Install Invoke-Build shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests shell: pwsh - run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + run: | Invoke-Build Test - name: Test docker builds From 5cdb7c8cab6004da2c4fd9b7a8de8b7648e09472 Mon Sep 17 00:00:00 2001 From: mdaneri Date: Sat, 30 Mar 2024 18:27:11 -0700 Subject: [PATCH 54/84] final --- .github/workflows/PSScriptAnalyzer.yml | 2 +- .github/workflows/ci-coverage.yml | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/PSScriptAnalyzer.yml b/.github/workflows/PSScriptAnalyzer.yml index 9372857fd..10a80b7ce 100644 --- a/.github/workflows/PSScriptAnalyzer.yml +++ b/.github/workflows/PSScriptAnalyzer.yml @@ -39,7 +39,7 @@ jobs: path: .\ recurse: true # Include your own basic security rules. Removing this option will run all the rules - includeRule: '"PSAvoidGlobalAliases", "PSAvoidUsingConvertToSecureStringWithPlainText"' + includeRule: '"PSAvoidUsingCmdletAliases" ,"PSAvoidUsingPlainTextForPassword","PSAvoidUsingWriteHost","PSAvoidUsingInvokeExpression","PSUseShouldProcessForStateChangingFunctions","PSAvoidUsingUsernameAndPasswordParams","PSUseProcessBlockForPipelineCommand","PSAvoidUsingConvertToSecureStringWithPlainText","PSUseSingularNouns","PSReviewUnusedParameter"' output: results.sarif # Upload the SARIF file generated in the previous step diff --git a/.github/workflows/ci-coverage.yml b/.github/workflows/ci-coverage.yml index 1a8de05a2..c31363e51 100644 --- a/.github/workflows/ci-coverage.yml +++ b/.github/workflows/ci-coverage.yml @@ -22,10 +22,14 @@ jobs: run: | $PSVersionTable.PSVersion + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + - name: Install Invoke-Build shell: pwsh run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Install-Module -Name InvokeBuild -RequiredVersion '5.10.5' -Force - name: Run Pester Tests @@ -34,5 +38,4 @@ jobs: PODE_COVERALLS_TOKEN: ${{ secrets.PODE_COVERALLS_TOKEN }} PODE_RUN_CODE_COVERAGE: false run: | - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Build Test \ No newline at end of file From ffb6980ab570d0287d9066a07493afd34435283d Mon Sep 17 00:00:00 2001 From: mdaneri <17148649+mdaneri@users.noreply.github.com> Date: Sun, 31 Mar 2024 20:13:04 +0000 Subject: [PATCH 55/84] Rename $Context.Server.DefaultFolders.Views --- src/Private/Context.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Private/Context.ps1 b/src/Private/Context.ps1 index 186d0cee0..699a4a9b7 100644 --- a/src/Private/Context.ps1 +++ b/src/Private/Context.ps1 @@ -826,8 +826,8 @@ function Set-PodeServerConfiguration { if ($Configuration.DefaultFolders.Public) { $Context.Server.DefaultFolders.Public = $Configuration.DefaultFolders.Public } - if ($Configuration.DefaultFolders.View) { - $Context.Server.DefaultFolders.View = $Configuration.DefaultFolders.View + if ($Configuration.DefaultFolders.Views) { + $Context.Server.DefaultFolders.Views = $Configuration.DefaultFolders.Views } if ($Configuration.DefaultFolders.Errors) { $Context.Server.DefaultFolders.Errors = $Configuration.DefaultFolders.Errors From e27dd174188fa7b5f2207aab05650627114fcdda Mon Sep 17 00:00:00 2001 From: mdaneri Date: Mon, 1 Apr 2024 18:35:36 -0700 Subject: [PATCH 56/84] first review --- docs/Tutorials/Configuration.md | 2 +- .../Routes/Utilities/StaticContent.md | 4 +- src/Misc/default-file-browsing.html.pode | 4 +- src/Private/Middleware.ps1 | 2 +- src/Private/Responses.ps1 | 211 +++++++++--------- 5 files changed, 109 insertions(+), 114 deletions(-) diff --git a/docs/Tutorials/Configuration.md b/docs/Tutorials/Configuration.md index 6e38b2876..10fc3be88 100644 --- a/docs/Tutorials/Configuration.md +++ b/docs/Tutorials/Configuration.md @@ -77,7 +77,7 @@ A "path" like `Server.Ssl.Protocols` looks like the below in the file: | Server.Root | Overrides root path of the server | [link](../Misc/ServerRoot) | | Server.Restart | Defines configuration for automatically restarting the server | [link](../Restarting/Types/AutoRestarting) | | Server.FileMonitor | Defines configuration for restarting the server based on file updates | [link](../Restarting/Types/FileMonitoring) | -| Server.RouteOrderMainBeforeStatic | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | +| Server.Web.Static.ValidateLast | Changes the way routes are processed. | [link](../Routes/Utilities/StaticContent) | | Web.TransferEncoding | Sets the Request TransferEncoding | [link](../Compression/Requests) | | Web.Compression | Sets any compression to use on the Response | [link](../Compression/Responses) | | Web.ContentType | Define expected Content Types for certain Routes | [link](../Routes/Utilities/ContentTypes) | diff --git a/docs/Tutorials/Routes/Utilities/StaticContent.md b/docs/Tutorials/Routes/Utilities/StaticContent.md index edfedc5f0..d05ef5413 100644 --- a/docs/Tutorials/Routes/Utilities/StaticContent.md +++ b/docs/Tutorials/Routes/Utilities/StaticContent.md @@ -181,7 +181,7 @@ Start-PodeServer -ScriptBlock { } ``` -When used with `-Download,` the browser downloads any file selected instead of rendering. The folders are rendered and not downloaded. +When used with `-DownloadOnly`, the browser downloads any file selected instead of rendering. The folders are rendered and not downloaded. ## Static Routes order By default, Static routes are processed before any other route. @@ -204,4 +204,4 @@ Nothing to report :D } ``` -To change the default behavior, you can use the `Server.RouteOrderMainBeforeStatic` property in the `server.psd1` configuration file, setting the value to `$True.` This will ensure that any static route is evaluated after any other route. \ No newline at end of file +To change the default behavior, you can use the `Server.Web.Static.ValidateLast` property in the `server.psd1` configuration file, setting the value to `$True.` This will ensure that any static route is evaluated after any other route. \ No newline at end of file diff --git a/src/Misc/default-file-browsing.html.pode b/src/Misc/default-file-browsing.html.pode index b24ef7b10..d2fb70df9 100644 --- a/src/Misc/default-file-browsing.html.pode +++ b/src/Misc/default-file-browsing.html.pode @@ -9,8 +9,8 @@ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/create-routes.ps1 b/examples/create-routes.ps1 new file mode 100644 index 000000000..8ae84cd7c --- /dev/null +++ b/examples/create-routes.ps1 @@ -0,0 +1,34 @@ + +#crete routes using different approaches +$ScriptPath=Split-Path -Parent -Path $MyInvocation.MyCommand.Path +$path = Split-Path -Parent -Path $ScriptPath +if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +} else { + Import-Module -Name 'Pode' +} + + +Start-PodeServer -Threads 1 -ScriptBlock { + Add-PodeEndpoint -Address localhost -Port 8081 -Protocol Http + New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging + + Add-PodeRoute -PassThru -Method Get -Path '/routeCreateScriptBlock/:id' -ScriptBlock ([ScriptBlock]::Create( (Get-Content -Path "$ScriptPath\scripts\routeScript.ps1" -Raw))) | + Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeCreateScriptBlock' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + + + Add-PodeRoute -PassThru -Method Post -Path '/routeFilePath/:id' -FilePath '.\scripts\routeScript.ps1' | Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeFilePath' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + + + Add-PodeRoute -PassThru -Method Get -Path '/routeScriptBlock/:id' -ScriptBlock { $Id = $WebEvent.Parameters['id'] ; Write-PodeJsonResponse -StatusCode 200 -Value @{'id' = $Id } } | + Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeScriptBlock' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + + + Add-PodeRoute -PassThru -Method Get -Path '/routeScriptSameScope/:id' -ScriptBlock { . $ScriptPath\scripts\routeScript.ps1 } | + Set-PodeOARouteInfo -Summary 'Test' -OperationId 'routeScriptSameScope' -PassThru | + Set-PodeOARequest -Parameters @((New-PodeOAStringProperty -Name 'id' | ConvertTo-PodeOAParameter -In Path -Required) ) + +} \ No newline at end of file diff --git a/examples/logging.ps1 b/examples/logging.ps1 index a752fad8a..65dfe4a02 100644 --- a/examples/logging.ps1 +++ b/examples/logging.ps1 @@ -1,15 +1,19 @@ $path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - +if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +} +else { + Import-Module -Name 'Pode' +} # or just: # Import-Module Pode -$LOGGING_TYPE = 'Terminal' # Terminal, File, Custom +$LOGGING_TYPE = 'terminal' # Terminal, File, Custom # create a server, and start listening on port 8085 Start-PodeServer { - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http Set-PodeViewEngine -Type Pode switch ($LOGGING_TYPE.ToLowerInvariant()) { diff --git a/examples/scripts/routeScript.ps1 b/examples/scripts/routeScript.ps1 new file mode 100644 index 000000000..6ced6153f --- /dev/null +++ b/examples/scripts/routeScript.ps1 @@ -0,0 +1,4 @@ +{ + $Id = $WebEvent.Parameters['id'] + Write-PodeJsonResponse -StatusCode 200 -Value @{'id' = $Id } +} \ No newline at end of file diff --git a/examples/web-auth-basic.ps1 b/examples/web-auth-basic.ps1 index fed990bee..1ac920dd4 100644 --- a/examples/web-auth-basic.ps1 +++ b/examples/web-auth-basic.ps1 @@ -1,5 +1,10 @@ $path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +} +else { + Import-Module -Name 'Pode' +} # or just: # Import-Module Pode @@ -23,7 +28,7 @@ Invoke-RestMethod -Uri http://localhost:8085/users -Method Post -Headers @{ Auth Start-PodeServer -Threads 2 { # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http # request logging New-PodeLoggingMethod -Terminal -Batch 10 -BatchTimeout 10 | Enable-PodeRequestLogging diff --git a/examples/web-auth-form-access.ps1 b/examples/web-auth-form-access.ps1 index a7551dd30..de372faa3 100644 --- a/examples/web-auth-form-access.ps1 +++ b/examples/web-auth-form-access.ps1 @@ -1,8 +1,10 @@ $path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode +if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +} +else { + Import-Module -Name 'Pode' +} <# This examples shows how to use session persistant authentication with access. @@ -22,7 +24,7 @@ take you back to the login page. Start-PodeServer -Threads 2 { # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http # set the view engine Set-PodeViewEngine -Type Pode diff --git a/examples/web-auth-oauth2-form.ps1 b/examples/web-auth-oauth2-form.ps1 index 4bf3a0b75..6cbc1baa6 100644 --- a/examples/web-auth-oauth2-form.ps1 +++ b/examples/web-auth-oauth2-form.ps1 @@ -18,7 +18,7 @@ Note: You'll need to register a new app in Azure, and note you clientId, secret, Start-PodeServer -Threads 2 { # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http -Default + Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http -Default New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging # set the view engine diff --git a/examples/web-rest-openapi-funcs.ps1 b/examples/web-rest-openapi-funcs.ps1 index 9f6c3a411..c50b72343 100644 --- a/examples/web-rest-openapi-funcs.ps1 +++ b/examples/web-rest-openapi-funcs.ps1 @@ -5,8 +5,14 @@ Start-PodeServer { Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger -DarkMode - Enable-PodeOpenApiViewer -Type ReDoc + Enable-PodeOpenApiViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOpenApiViewer -Type ReDoc -Path '/docs/redoc' + Enable-PodeOpenApiViewer -Type RapiDoc -Path '/docs/rapidoc' + Enable-PodeOpenApiViewer -Type StopLight -Path '/docs/stoplight' + Enable-PodeOpenApiViewer -Type Explorer -Path '/docs/explorer' + Enable-PodeOpenApiViewer -Type RapiPdf -Path '/docs/rapipdf' + + Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' #ConvertTo-PodeRoute -Path '/api' -Commands @('Get-ChildItem', 'New-Item') ConvertTo-PodeRoute -Path '/api' -Module Pester diff --git a/examples/web-rest-openapi-shared.ps1 b/examples/web-rest-openapi-shared.ps1 index 1054014b2..3571801c0 100644 --- a/examples/web-rest-openapi-shared.ps1 +++ b/examples/web-rest-openapi-shared.ps1 @@ -1,5 +1,6 @@ $path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +#Import-Module -Name powershell-yaml -Force -ErrorAction Stop Start-PodeServer { Add-PodeEndpoint -Address localhost -Port 8080 -Protocol Http -Name 'user' @@ -8,8 +9,16 @@ Start-PodeServer { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger -DarkMode + Enable-PodeOpenApiViewer -Type Swagger Enable-PodeOpenApiViewer -Type ReDoc + Enable-PodeOpenApiViewer -Type RapiDoc + Enable-PodeOpenApiViewer -Type StopLight + Enable-PodeOpenApiViewer -Type Explorer + Enable-PodeOpenApiViewer -Type RapiPdf + + + Enable-PodeOpenApiViewer -Editor + Enable-PodeOpenApiViewer -Bookmarks New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { @@ -19,7 +28,7 @@ Start-PodeServer { if ($username -eq 'morty' -and $password -eq 'pickle') { return @{ User = @{ - ID ='M0R7Y302' + ID = 'M0R7Y302' Name = 'Morty' Type = 'Human' } @@ -34,59 +43,58 @@ Start-PodeServer { 'application/json' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'Name'), (New-PodeOAIntProperty -Name 'UserId') - )) + )) } New-PodeOAIntProperty -Name 'userId' -Required | - ConvertTo-PodeOAParameter -In Path | - Add-PodeOAComponentParameter -Name 'UserId' - + ConvertTo-PodeOAParameter -In Path | + Add-PodeOAComponentParameter -Name 'UserId' Add-PodeAuthMiddleware -Name AuthMiddleware -Authentication Validate -Route '/api/*' - - Add-PodeRoute -Method Get -Path "/api/resources" -EndpointName 'user' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/api/resources' -EndpointName 'user' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = 123 } } -PassThru | - Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Resources' -PassThru | - Add-PodeOAResponse -StatusCode 200 -Reference 'OK' + Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Resources' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'OK' - Add-PodeRoute -Method Post -Path "/api/resources" -ScriptBlock { + Add-PodeRoute -Method Post -Path '/api/resources' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = 123 } } -PassThru | - Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Resources' -PassThru | - Add-PodeOAResponse -StatusCode 200 -Reference 'OK' + Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Resources' -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'OK' Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Parameters['userId'] } } -PassThru | - Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Users' -PassThru | - Set-PodeOARequest -Parameters @( + Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Users' -PassThru | + Set-PodeOARequest -Parameters @( (ConvertTo-PodeOAParameter -Reference 'UserId') - ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Reference 'OK' + ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'OK' Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Query['userId'] } } -PassThru | - Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Users' -PassThru | - Set-PodeOARequest -Parameters @( + Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Users' -PassThru | + Set-PodeOARequest -Parameters @( (New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Query) - ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Reference 'OK' + ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'OK' Add-PodeRoute -Method Post -Path '/api/users' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Data.userId } } -PassThru | - Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Users' -PassThru | - Set-PodeOARequest -RequestBody ( - New-PodeOARequestBody -Required -ContentSchemas @{ - 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) - } - ) -PassThru | - Add-PodeOAResponse -StatusCode 200 -Reference 'OK' + Set-PodeOARouteInfo -Summary 'A cool summary' -Tags 'Users' -PassThru | + Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Required -ContentSchemas @{ + 'application/json' = (New-PodeOAIntProperty -Name 'userId' -Object) + } + ) -PassThru | + Add-PodeOAResponse -StatusCode 200 -Reference 'OK' + } \ No newline at end of file diff --git a/examples/web-rest-openapi-simple.ps1 b/examples/web-rest-openapi-simple.ps1 index a578b1547..072e7eba2 100644 --- a/examples/web-rest-openapi-simple.ps1 +++ b/examples/web-rest-openapi-simple.ps1 @@ -8,43 +8,43 @@ Start-PodeServer { Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes Enable-PodeOpenApiViewer -Type Swagger -DarkMode Enable-PodeOpenApiViewer -Type ReDoc + Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' - - Add-PodeRoute -Method Get -Path "/api/resources" -EndpointName 'user' -ScriptBlock { + Add-PodeRoute -Method Get -Path '/api/resources' -EndpointName 'user' -ScriptBlock { Set-PodeResponseStatus -Code 200 } - Add-PodeRoute -Method Post -Path "/api/resources" -ScriptBlock { + Add-PodeRoute -Method Post -Path '/api/resources' -ScriptBlock { Set-PodeResponseStatus -Code 200 } Add-PodeRoute -Method Get -Path '/api/users/:userId' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Parameters['userId'] } - } -PassThru | - Set-PodeOARequest -Parameters @( - (New-PodeOAIntProperty -Name 'userId' -Enum @(100,300,999) -Required | ConvertTo-PodeOAParameter -In Path) - ) + } -PassThru | Set-PodeOARouteInfo -PassThru | + Set-PodeOARequest -Parameters @( + (New-PodeOAIntProperty -Name 'userId' -Enum @(100, 300, 999) -Required | ConvertTo-PodeOAParameter -In Path) + ) Add-PodeRoute -Method Get -Path '/api/users' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = 'Rick'; UserId = $WebEvent.Query['userId'] } - } -PassThru | - Set-PodeOARequest -Parameters @( + } -PassThru | Set-PodeOARouteInfo -PassThru | + Set-PodeOARequest -Parameters @( (New-PodeOAIntProperty -Name 'userId' -Required | ConvertTo-PodeOAParameter -In Query) - ) + ) Add-PodeRoute -Method Post -Path '/api/users' -ScriptBlock { Write-PodeJsonResponse -Value @{ Name = $WebEvent.Data.Name; UserId = $WebEvent.Data.UserId } - } -PassThru | - Set-PodeOARequest -RequestBody ( - New-PodeOARequestBody -Required -ContentSchemas @{ - 'application/json' = (New-PodeOAObjectProperty -Properties @( + } -PassThru | Set-PodeOARouteInfo -PassThru | + Set-PodeOARequest -RequestBody ( + New-PodeOARequestBody -Required -ContentSchemas @{ + 'application/json' = (New-PodeOAObjectProperty -Properties @( (New-PodeOAStringProperty -Name 'Name' -MaxLength 5 -Pattern '[a-zA-Z]+'), (New-PodeOAIntProperty -Name 'UserId') )) - } - ) + } + ) } diff --git a/examples/web-rest-openapi.ps1 b/examples/web-rest-openapi.ps1 index 1e51408d8..0f6e25598 100644 --- a/examples/web-rest-openapi.ps1 +++ b/examples/web-rest-openapi.ps1 @@ -8,9 +8,14 @@ Start-PodeServer { New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging Enable-PodeOpenApi -Title 'OpenAPI Example' -RouteFilter '/api/*' -RestrictRoutes - Enable-PodeOpenApiViewer -Type Swagger -DarkMode - Enable-PodeOpenApiViewer -Type ReDoc - + Enable-PodeOpenApiViewer -Type Swagger -Path '/docs/swagger' + Enable-PodeOpenApiViewer -Type ReDoc -Path '/docs/redoc' + Enable-PodeOpenApiViewer -Type RapiDoc -Path '/docs/rapidoc' + Enable-PodeOpenApiViewer -Type StopLight -Path '/docs/stoplight' + Enable-PodeOpenApiViewer -Type Explorer -Path '/docs/explorer' + Enable-PodeOpenApiViewer -Type RapiPdf -Path '/docs/rapipdf' + Enable-PodeOpenApiViewer -Editor -Path '/docs/editor' + Enable-PodeOpenApiViewer -Bookmarks -Path '/docs' New-PodeAuthScheme -Basic | Add-PodeAuth -Name 'Validate' -Sessionless -ScriptBlock { return @{ diff --git a/examples/web-static-auth.ps1 b/examples/web-static-auth.ps1 index 8e16d9c67..b065771b0 100644 --- a/examples/web-static-auth.ps1 +++ b/examples/web-static-auth.ps1 @@ -1,14 +1,16 @@ $path = Split-Path -Parent -Path (Split-Path -Parent -Path $MyInvocation.MyCommand.Path) -Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop - -# or just: -# Import-Module Pode +if (Test-Path -Path "$($path)/src/Pode.psm1" -PathType Leaf) { + Import-Module "$($path)/src/Pode.psm1" -Force -ErrorAction Stop +} +else { + Import-Module -Name 'Pode' +} # create a server, and start listening on port 8085 Start-PodeServer -Threads 2 { # listen on localhost:8085 - Add-PodeEndpoint -Address * -Port 8085 -Protocol Http + Add-PodeEndpoint -Address localhost -Port 8085 -Protocol Http New-PodeLoggingMethod -Terminal | Enable-PodeRequestLogging New-PodeLoggingMethod -Terminal | Enable-PodeErrorLogging diff --git a/packers/docker/arm32/Dockerfile b/packers/docker/arm32/Dockerfile index 9f842034a..8cc8c8b7f 100644 --- a/packers/docker/arm32/Dockerfile +++ b/packers/docker/arm32/Dockerfile @@ -1,6 +1,6 @@ FROM arm32v7/ubuntu:bionic -ENV PS_VERSION=7.3.1 +ENV PS_VERSION=7.3.11 ENV PS_PACKAGE=powershell-${PS_VERSION}-linux-arm32.tar.gz ENV PS_PACKAGE_URL=https://github.com/PowerShell/PowerShell/releases/download/v${PS_VERSION}/${PS_PACKAGE} diff --git a/src/Misc/default-doc-bookmarks.html.pode b/src/Misc/default-doc-bookmarks.html.pode new file mode 100644 index 000000000..d9418eacc --- /dev/null +++ b/src/Misc/default-doc-bookmarks.html.pode @@ -0,0 +1,280 @@ + + + + + OpenAPI Documentation Bookmarks + + + + + + + + + + + +

$($data.Title)

+

Documentation

+ + +
+

OpenAPI Definition

+ + +
+ +
+ + +
+ +
+
Loading JSON content...
+
+ +
+ 🧡 Powered by Pode +
+ + + + \ No newline at end of file diff --git a/src/Misc/default-explorer.html.pode b/src/Misc/default-explorer.html.pode new file mode 100644 index 000000000..7954646d5 --- /dev/null +++ b/src/Misc/default-explorer.html.pode @@ -0,0 +1,60 @@ + + + + + + + $($data.Title) + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Misc/default-file-browsing.html.pode b/src/Misc/default-file-browsing.html.pode index f27780657..2c5c6103b 100644 --- a/src/Misc/default-file-browsing.html.pode +++ b/src/Misc/default-file-browsing.html.pode @@ -1,6 +1,6 @@ - + File Browser:$($Data.Path) diff --git a/src/Misc/default-rapidoc.html.pode b/src/Misc/default-rapidoc.html.pode new file mode 100644 index 000000000..a12aa9b62 --- /dev/null +++ b/src/Misc/default-rapidoc.html.pode @@ -0,0 +1,16 @@ + + + + + $($data.Title) + + + + + + + + + + \ No newline at end of file diff --git a/src/Misc/default-rapipdf.html.pode b/src/Misc/default-rapipdf.html.pode new file mode 100644 index 000000000..1268b051c --- /dev/null +++ b/src/Misc/default-rapipdf.html.pode @@ -0,0 +1,150 @@ + + + + + RapiPdf Web Page + + + + + + + + +
+

RapiPdf Configuration

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
DescriptionValue
Colour used for headings of main sections in PDF
Colour used for sub-headings
Title of the generated PDF
Text to be printed at the bottom of every page
Include the info section in the generated PDF? + + +
Include a table of contents in the generated PDF? + + +
Include the security section in the generated PDF? + + +
Include all API details in the generated PDF? + + +
Include a list of all APIs and their summaries at the end of the generated PDF? + + +
Include OpenAPI specified examples in the generated PDF? + + +
+
+ +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/src/Misc/default-redoc.html.pode b/src/Misc/default-redoc.html.pode index f179bf848..929975a08 100644 --- a/src/Misc/default-redoc.html.pode +++ b/src/Misc/default-redoc.html.pode @@ -1,20 +1,22 @@ - - - $($data.Title) - - - + + + $($data.Title) + + + + + + + + + + + - - - - - - \ No newline at end of file diff --git a/src/Misc/default-stoplight.html.pode b/src/Misc/default-stoplight.html.pode new file mode 100644 index 000000000..ce94f72ec --- /dev/null +++ b/src/Misc/default-stoplight.html.pode @@ -0,0 +1,19 @@ + + + + + + + $($data.Title) + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Misc/default-swagger-editor.html.pode b/src/Misc/default-swagger-editor.html.pode new file mode 100644 index 000000000..31f52981b --- /dev/null +++ b/src/Misc/default-swagger-editor.html.pode @@ -0,0 +1,106 @@ + + + + + + Swagger Editor + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Misc/default-swagger.html.pode b/src/Misc/default-swagger.html.pode index 7cdd320f0..15b3ebefa 100644 --- a/src/Misc/default-swagger.html.pode +++ b/src/Misc/default-swagger.html.pode @@ -1,11 +1,11 @@ - - + + $($data.Title) - - + +