Tests/Helpers.Tests.ps1
|
#Requires -Version 5.1 # Pester v5 unit tests for the N2C.GuacAdmin private helpers (no network). # Dot-source the private helpers in BeforeAll so that they resolve inside the # It blocks (Pester v5 runs the file body in a discovery-only scope, so the # helpers and their paths must be established in the runtime scope). BeforeAll { $moduleRoot = Split-Path -Parent $PSScriptRoot # Import the module first so type variables are set for helpers that use them Import-Module (Join-Path $moduleRoot 'N2C.GuacAdmin.psd1') -Force # Set type variables in this script scope so dot-sourced helpers that # reference $script:Guac*Type variables can find them (AGENTS.md §6.1) $script:GuacInstructionType = [N2C_GuacAdmin_GuacInstruction] $script:GuacActiveSessionType = [N2C_GuacAdmin_GuacActiveSession] $script:GuacRestExceptionType = [N2C_GuacAdmin_GuacRestException] foreach ($scriptFile in 'ConvertTo-GuacServerUrl', 'ConvertTo-GuacFormUrlEncoded', 'ConvertTo-GuacMaskedSecret', 'ConvertFrom-GuacSecureString', 'ConvertTo-GuacJson', 'Get-GuacResponseDetails', 'ConvertFrom-GuacErrorBody', 'ConvertTo-GuacInstructionString', 'ConvertFrom-GuacInstructionString') { . (Join-Path $moduleRoot ('Private/{0}.ps1' -f $scriptFile)) } } Describe 'ConvertTo-GuacServerUrl' { It 'accepts a bare https URL' { ConvertTo-GuacServerUrl -Server 'https://guac.example.com' | Should -Be 'https://guac.example.com' } It 'preserves a context path and removes a trailing slash' { ConvertTo-GuacServerUrl -Server 'https://guac.example.com/guacamole/' | Should -Be 'https://guac.example.com/guacamole' } It 'accepts http' { ConvertTo-GuacServerUrl -Server 'http://127.0.0.1:8080/guacamole' | Should -Be 'http://127.0.0.1:8080/guacamole' } It 'rejects an empty value' { { ConvertTo-GuacServerUrl -Server '' } | Should -Throw } It 'rejects a relative value' { { ConvertTo-GuacServerUrl -Server 'guac.example.com/guacamole' } | Should -Throw } It 'rejects a non-http(s) scheme' { { ConvertTo-GuacServerUrl -Server 'ftp://guac.example.com' } | Should -Throw } } Describe 'ConvertTo-GuacRestUrl' { It 'joins base and path' { ConvertTo-GuacRestUrl -Server 'https://guac.example.com/guacamole/' -Path '/api/session' | Should -Be 'https://guac.example.com/guacamole/api/session' } It 'adds a leading slash when missing' { ConvertTo-GuacRestUrl -Server 'https://guac.example.com' -Path 'api/session' | Should -Be 'https://guac.example.com/api/session' } It 'preserves an existing query string' { ConvertTo-GuacRestUrl -Server 'https://guac.example.com' -Path '/api/x?contains=a&order=startDate' | Should -Be 'https://guac.example.com/api/x?contains=a&order=startDate' } } Describe 'ConvertTo-GuacFormUrlEncoded' { It 'encodes simple fields' { ConvertTo-GuacFormUrlEncoded -Fields ([ordered]@{ username = 'guacadmin'; password = 'secret' }) | Should -Be 'username=guacadmin&password=secret' } It 'percent-encodes special characters' { $encoded = ConvertTo-GuacFormUrlEncoded -Fields ([ordered]@{ password = 'p@ss w&rd/ç' }) $decoded = [uri]::UnescapeDataString($encoded.Substring('password='.Length)) $decoded | Should -Be 'p@ss w&rd/ç' } It 'skips null values (optional guac-totp)' { ConvertTo-GuacFormUrlEncoded -Fields ([ordered]@{ username = 'a'; password = 'b'; 'guac-totp' = $null }) | Should -Be 'username=a&password=b' } It 'includes guac-totp when present' { ConvertTo-GuacFormUrlEncoded -Fields ([ordered]@{ username = 'a'; password = 'b'; 'guac-totp' = '123456' }) | Should -Be 'username=a&password=b&guac-totp=123456' } } Describe 'ConvertTo-GuacMaskedSecret' { It 'masks empty values entirely' { ConvertTo-GuacMaskedSecret -Value '' | Should -Be '****' } It 'masks short values entirely' { ConvertTo-GuacMaskedSecret -Value 'secret8' | Should -Be '****' } It 'keeps the first and last 4 characters of long values' { ConvertTo-GuacMaskedSecret -Value '3E6F9F7EDD15D99EB7CF28AFE6EED' | Should -Be '3E6F*********************6EED' } } Describe 'ConvertFrom-GuacSecureString' { It 'converts a SecureString to plaintext' { $secure = ConvertTo-SecureString 'topSecretValue' -AsPlainText -Force ConvertFrom-GuacSecureString -SecureString $secure | Should -Be 'topSecretValue' } } Describe 'ConvertTo-GuacJson' { It 'serializes a hashtable' { $json = ConvertTo-GuacJson -InputObject @{ name = 'x'; value = 1 } $parsed = $json | ConvertFrom-Json $parsed.name | Should -Be 'x' $parsed.value | Should -Be 1 } It 'preserves a single-element array (5.1 unwrapping guard)' { $json = ConvertTo-GuacJson -InputObject (@([ordered]@{ op = 'add'; path = '/new' })) $json | Should -BeLike '[[?]*]' $parsed = $json | ConvertFrom-Json $parsed | Should -BeOfType [System.Management.Automation.PSObject] # Re-parse check: it must round-trip as an array with one element. $json | Should -Match '^\[' } It 'serializes an empty array as []' { ConvertTo-GuacJson -InputObject @() | Should -Be '[]' } It 'passes strings through unchanged' { ConvertTo-GuacJson -InputObject 'plain text body' | Should -Be 'plain text body' } } Describe 'ConvertFrom-GuacErrorBody' { It 'parses an APIError JSON body' { $body = '{"message":"No such token.","type":"NOT_FOUND","statusCode":404}' $guacError = ConvertFrom-GuacErrorBody -Body $body $guacError.Type | Should -Be 'NOT_FOUND' $guacError.Message | Should -Be 'No such token.' $guacError.StatusCode | Should -Be 404 } It 'returns $null fields for a body without them' { $guacError = ConvertFrom-GuacErrorBody -Body '{"message":"boom"}' $guacError.Type | Should -BeNullOrEmpty $guacError.Message | Should -Be 'boom' } It 'treats a non-JSON body as a plain message' { $guacError = ConvertFrom-GuacErrorBody -Body '<html>Internal Server Error</html>' $guacError.Type | Should -BeNullOrEmpty $guacError.Message | Should -Be '<html>Internal Server Error</html>' } It 'handles an empty body' { $guacError = ConvertFrom-GuacErrorBody -Body '' $guacError.Message | Should -BeNullOrEmpty } } Describe 'Get-GuacErrorCategory' { # Assert by category name: the returned values are genuine # [ErrorCategory] enum values (verified via .Equals in the probe), but # 'Should -Be' against a [type]::Member expression is unreliable in this # Pester/PowerShell combination for enum-typed pipeline input. It 'maps 0 (transport failure) to ConnectionError' { (Get-GuacErrorCategory -StatusCode 0).ToString() | Should -Be 'ConnectionError' } It 'maps 401 to SecurityError' { (Get-GuacErrorCategory -StatusCode 401).ToString() | Should -Be 'SecurityError' } It 'maps 403 to SecurityError' { (Get-GuacErrorCategory -StatusCode 403).ToString() | Should -Be 'SecurityError' } It 'maps 404 to InvalidArgument' { (Get-GuacErrorCategory -StatusCode 404).ToString() | Should -Be 'InvalidArgument' } It 'maps 429 (rate limited) to InvalidArgument' { (Get-GuacErrorCategory -StatusCode 429).ToString() | Should -Be 'InvalidArgument' } It 'maps 500 to ResourceUnavailable' { (Get-GuacErrorCategory -StatusCode 500).ToString() | Should -Be 'ResourceUnavailable' } It 'maps 503 to ResourceUnavailable' { (Get-GuacErrorCategory -StatusCode 503).ToString() | Should -Be 'ResourceUnavailable' } } Describe 'Get-GuacResponseHeaderValue' { It 'looks up headers case-insensitively' { $headers = [ordered]@{ 'Content-Type' = 'application/json' } Get-GuacResponseHeaderValue -Headers $headers -Name 'content-type' | Should -Be 'application/json' } It 'returns $null when the header is absent' { $headers = [ordered]@{ 'X-Other' = 'v' } Get-GuacResponseHeaderValue -Headers $headers -Name 'Content-Type' | Should -BeNullOrEmpty } It 'returns $null for null headers' { Get-GuacResponseHeaderValue -Headers $null -Name 'Content-Type' | Should -BeNullOrEmpty } } Describe 'ConvertTo-GuacInstructionString' { BeforeAll { # Import the module so type literals resolve Import-Module (Join-Path $moduleRoot 'N2C.GuacAdmin.psd1') -Force . (Join-Path $moduleRoot 'Private/ConvertTo-GuacInstructionString.ps1') } It 'encodes a simple instruction with no arguments' { $instr = [N2C_GuacAdmin_GuacInstruction]::new('select', @()) $result = ConvertTo-GuacInstructionString -Instruction $instr $result | Should -Be '6.select;' } It 'encodes an instruction with arguments' { $instr = [N2C_GuacAdmin_GuacInstruction]::new('select', @('abc-123')) $result = ConvertTo-GuacInstructionString -Instruction $instr $result | Should -Be '6.select,7.abc-123;' } It 'encodes a size instruction with multiple numeric arguments' { $instr = [N2C_GuacAdmin_GuacInstruction]::new('size', @('1024', '768', '96')) $result = ConvertTo-GuacInstructionString -Instruction $instr $result | Should -Be '4.size,4.1024,3.768,2.96;' } It 'encodes using -Opcode and -Arguments directly' { $result = ConvertTo-GuacInstructionString -Opcode 'key' -Arguments @('1', '65', 'true') $result | Should -Be '3.key,1.1,2.65,4.true;' } It 'handles empty string arguments' { $instr = [N2C_GuacAdmin_GuacInstruction]::new('name', @('')) $result = ConvertTo-GuacInstructionString -Instruction $instr $result | Should -Be '4.name,0.;' } } Describe 'ConvertFrom-GuacInstructionString' { BeforeAll { Import-Module (Join-Path $moduleRoot 'N2C.GuacAdmin.psd1') -Force . (Join-Path $moduleRoot 'Private/ConvertFrom-GuacInstructionString.ps1') } It 'decodes a simple instruction with no arguments' { $instr = ConvertFrom-GuacInstructionString -Raw '6.select;' $instr.Opcode | Should -Be 'select' $instr.Arguments.Length | Should -Be 0 } It 'decodes an instruction with arguments' { $instr = ConvertFrom-GuacInstructionString -Raw '6.select,7.abc-123;' $instr.Opcode | Should -Be 'select' $instr.Arguments.Length | Should -Be 1 $instr.Arguments[0] | Should -Be 'abc-123' } It 'decodes a size instruction with multiple arguments' { $instr = ConvertFrom-GuacInstructionString -Raw '4.size,4.1024,3.768,2.96;' $instr.Opcode | Should -Be 'size' $instr.Arguments.Length | Should -Be 3 $instr.Arguments[0] | Should -Be '1024' $instr.Arguments[1] | Should -Be '768' $instr.Arguments[2] | Should -Be '96' } It 'decodes an instruction with empty string arguments' { $instr = ConvertFrom-GuacInstructionString -Raw '4.name,0.;' $instr.Opcode | Should -Be 'name' $instr.Arguments.Length | Should -Be 1 $instr.Arguments[0] | Should -Be '' } It 'handles the ready instruction with session ID' { $instr = ConvertFrom-GuacInstructionString -Raw '5.ready,36.123e4567-e89b-12d3-a456-426614174000;' $instr.Opcode | Should -Be 'ready' $instr.Arguments[0] | Should -Be '123e4567-e89b-12d3-a456-426614174000' } } |