tests/functions/ConvertTo-QueryString.Tests.ps1
|
BeforeAll { . "$PSScriptRoot/../../internal/ConvertTo-QueryString.ps1" } Describe 'ConvertTo-QueryString' { Context 'Hashtable input' { It 'Should convert simple hashtable to query string' { $hashtable = @{ name = 'test'; value = '123' } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'name=test' $result | Should -Match 'value=123' $result | Should -Match '&' } It 'Should handle empty hashtable' { $hashtable = @{} $result = ConvertTo-QueryString $hashtable $result | Should -Be '' } It 'Should URL encode values with special characters' { $hashtable = @{ title = 'convert&prosper'; path = 'path/file.json' } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'title=convert%26prosper' $result | Should -Match 'path=path%2Ffile\.json' } It 'Should handle numeric values' { $hashtable = @{ index = 10; price = 99.99 } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'index=10' # Decimal separator can be . or , depending on locale $result | Should -Match 'price=99(\.|%2C)99' } It 'Should handle boolean values' { $hashtable = @{ enabled = $true; visible = $false } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'enabled=True' $result | Should -Match 'visible=False' } It 'Should handle GUID values' { $guid = [guid]'352182e6-9ab0-4115-807b-c36c88029fa4' $hashtable = @{ id = $guid } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'id=352182e6-9ab0-4115-807b-c36c88029fa4' } It 'Should handle null/empty values' { $hashtable = @{ empty = ''; null = $null } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'empty=' $result | Should -Match 'null=' } } Context 'OrderedDictionary input' { It 'Should convert ordered dictionary to query string' { $ordered = [ordered]@{ first = 'value1'; second = 'value2'; third = 'value3' } $result = ConvertTo-QueryString $ordered $result | Should -Match 'first=value1' $result | Should -Match 'second=value2' $result | Should -Match 'third=value3' } It 'Should maintain order with ordered dictionary' { $ordered = [ordered]@{ z = 'last'; a = 'first'; m = 'middle' } $result = ConvertTo-QueryString $ordered # The order should be preserved (z, a, m) $result.IndexOf('z=last') | Should -BeLessThan $result.IndexOf('a=first') $result.IndexOf('a=first') | Should -BeLessThan $result.IndexOf('m=middle') } } Context 'PSObject input' { It 'Should convert PSObject with properties to query string' { $obj = New-Object PSObject $obj | Add-Member -MemberType NoteProperty -Name 'name' -Value 'test' $obj | Add-Member -MemberType NoteProperty -Name 'value' -Value '456' $result = ConvertTo-QueryString $obj $result | Should -Match 'name=test' $result | Should -Match 'value=456' } It 'Should handle PSObject with empty properties' { $obj = New-Object PSObject $result = ConvertTo-QueryString $obj $result | Should -Be '' } } Context 'Pipeline input' { It 'Should accept hashtable from pipeline' { $hashtable = @{ test = 'pipeline' } $result = $hashtable | ConvertTo-QueryString $result | Should -Match 'test=pipeline' } It 'Should handle multiple objects from pipeline' { $hash1 = @{ first = 'value1' } $hash2 = @{ second = 'value2' } $results = @($hash1, $hash2) | ConvertTo-QueryString $results.Count | Should -Be 2 $results[0] | Should -Match 'first=value1' $results[1] | Should -Match 'second=value2' } } Context 'Special characters and encoding' { It 'Should properly encode spaces' { $hashtable = @{ text = 'hello world' } $result = ConvertTo-QueryString $hashtable $result | Should -eq 'text=hello+world' } It 'Should properly encode special URL characters' { $hashtable = @{ ampersand = 'test&value' equals = 'test=value' question = 'test?value' hash = 'test#value' plus = 'test+value' percent = 'test%value' } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'ampersand=test%26value' $result | Should -Match 'equals=test%3Dvalue' $result | Should -Match 'question=test%3Fvalue' $result | Should -Match 'hash=test%23value' $result | Should -Match 'plus=test%2Bvalue' $result | Should -Match 'percent=test%25value' } It 'Should handle Unicode characters' { $hashtable = @{ unicode = 'café' } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'unicode=caf%C3%A9' } } Context 'Return type and format' { It 'Should return string type' { $hashtable = @{ test = 'value' } $result = ConvertTo-QueryString $hashtable $result | Should -BeOfType [string] } It 'Should not start with question mark' { $hashtable = @{ param = 'value' } $result = ConvertTo-QueryString $hashtable $result | Should -Not -Match '^\?' } It 'Should separate parameters with ampersand' { $hashtable = @{ param1 = 'value1'; param2 = 'value2'; param3 = 'value3' } $result = ConvertTo-QueryString $hashtable ($result -split '&').Count | Should -Be 3 } } Context 'Microsoft Graph query parameters' { It 'Should handle $filter with startswith function' { $graphParams = @{ '$filter' = "startswith(givenName,'J')" } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$filter=startswith(givenName%2C%27J%27)' } It 'Should handle $select for specific properties' { $graphParams = @{ '$select' = 'givenName,surname,mail' } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$select=givenName%2Csurname%2Cmail' } It 'Should handle $orderby with ascending and descending' { $graphParams = @{ '$orderby' = 'displayName desc,givenName asc' } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$orderby=displayName+desc%2CgivenName+asc' } It 'Should handle $expand with nested select' { $graphParams = @{ '$expand' = 'members($select=id,displayName)' } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$expand=members(%24select%3Did%2CdisplayName)' } It 'Should handle $search query' { $graphParams = @{ '$search' = '"displayName:John"' } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$search=%22displayName%3AJohn%22' } It 'Should handle $count parameter' { $graphParams = @{ '$count' = 'true' '$top' = 10 } $result = ConvertTo-QueryString $graphParams $result | Should -Match ([regex]::escape('$count=true')) $result | Should -Match ([regex]::escape('$top=10')) } It 'Should handle complex $filter with multiple conditions' { $graphParams = @{ '$filter' = "userType eq 'Member' and accountEnabled eq true" } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$filter=userType+eq+%27Member%27+and+accountEnabled+eq+true' } It 'Should handle $filter with date comparison' { $graphParams = @{ '$filter' = "createdDateTime ge 2023-01-01T00:00:00Z" } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$filter=createdDateTime+ge+2023-01-01T00%3A00%3A00Z' } It 'Should handle $skipToken for pagination' { $graphParams = @{ '$skiptoken' = 'X%274453707402000100000017...' } $result = ConvertTo-QueryString $graphParams $result | Should -Be '$skiptoken=X%25274453707402000100000017...' } It 'Should handle advanced query with ConsistencyLevel header requirements' { $graphParams = @{ '$filter' = "endsWith(mail,'@contoso.com')" '$count' = 'true' '$orderby' = 'displayName' } $result = ConvertTo-QueryString $graphParams $result | Should -Match ([regex]::escape('$filter=endsWith(mail%2C%27%40contoso.com%27)')) $result | Should -Match ([regex]::escape('$count=true')) $result | Should -Match ([regex]::escape('$orderby=displayName')) } It 'Should handle $format parameter' { $graphParams = @{ '$format' = 'json' } $result = ConvertTo-QueryString $graphParams $result | Should -Match ([regex]::escape('$format=json')) } It 'Should handle mail-specific query with $search' { $graphParams = @{ '$search' = 'pizza' '$top' = 5 } $result = ConvertTo-QueryString $graphParams $result | Should -Match ([regex]::escape('$search=pizza')) $result | Should -Match ([regex]::escape('$top=5')) } } Context 'Edge cases and null value handling' { It 'Should handle null values correctly' { $hashtable = @{ validKey = 'validValue' nullKey = $null } $result = ConvertTo-QueryString $hashtable $result | Should -Match ([regex]::escape('validKey=validValue')) $result | Should -Match ([regex]::escape('nullKey=')) } It 'Should handle empty string values' { $hashtable = @{ emptyString = '' whitespace = ' ' } $result = ConvertTo-QueryString $hashtable $result | Should -Match ([regex]::escape('emptyString=')) $result | Should -Match ([regex]::escape('whitespace=+++')) } It 'Should handle hashtable with mixed null and valid values' { $hashtable = @{ name = 'John' middleName = $null lastName = 'Doe' nickname = '' } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'name=John' $result | Should -Match 'middleName=' $result | Should -Match 'lastName=Doe' $result | Should -Match 'nickname=' } It 'Should handle PSObject with null properties' { $obj = New-Object PSObject $obj | Add-Member -MemberType NoteProperty -Name 'validProp' -Value 'value' $obj | Add-Member -MemberType NoteProperty -Name 'nullProp' -Value $null $obj | Add-Member -MemberType NoteProperty -Name 'emptyProp' -Value '' $result = ConvertTo-QueryString $obj $result | Should -Match 'validProp=value' $result | Should -Match 'nullProp=' $result | Should -Match 'emptyProp=' } It 'Should handle very long parameter names and values' { $longName = 'a' * 1000 $longValue = 'b' * 2000 $hashtable = @{ $longName = $longValue } { ConvertTo-QueryString $hashtable } | Should -Not -Throw $result = ConvertTo-QueryString $hashtable $result | Should -Match "^$longName=" } It 'Should handle special parameter names that look like OData parameters' { $hashtable = @{ 'filter' = 'not-a-real-odata-filter' 'select' = 'fake-select' '$actualOData' = 'real-odata' } $result = ConvertTo-QueryString $hashtable $result | Should -Match ([regex]::escape('filter=not-a-real-odata-filter')) $result | Should -Match ([regex]::escape('select=fake-select')) $result | Should -Match ([regex]::escape('$actualOData=real-odata')) } It 'Should handle array-like values converted to string' { $hashtable = @{ arrayValue = @('item1', 'item2', 'item3') singleArray = @('single') } $result = ConvertTo-QueryString $hashtable # Arrays get converted to their string representation $result | Should -Match 'arrayValue=System\.Object%5B%5D' $result | Should -Match 'singleArray=System\.Object%5B%5D' } It 'Should handle DateTime values' { $dateTime = Get-Date '2023-12-25T10:30:00Z' $hashtable = @{ createdDate = $dateTime } $result = ConvertTo-QueryString $hashtable $result | Should -Match 'createdDate=' # DateTime gets converted to string and URL encoded } It 'Should handle single quotes in OData expressions' { $hashtable = @{ '$filter' = "subject eq 'let''s meet for lunch?'" } $result = ConvertTo-QueryString $hashtable $result | Should -Match ([regex]::escape('$filter=subject+eq+%27let%27%27s+meet+for+lunch%3F%27')) } It 'Should handle extremely large hashtable' { $largeHashtable = @{} for ($i = 1; $i -le 1000; $i++) { $largeHashtable["param$i"] = "value$i" } { ConvertTo-QueryString $largeHashtable } | Should -Not -Throw $result = ConvertTo-QueryString $largeHashtable ($result -split '&').Count | Should -Be 1000 } It 'Should handle nested object properties that become strings' { $nestedObj = @{ level1 = @{ level2 = 'deep value' } } $hashtable = @{ nested = $nestedObj } $result = ConvertTo-QueryString $hashtable # Nested objects get converted to their string representation $result | Should -Match 'nested=' } } Context 'Performance optimization tests' { It 'Should execute within acceptable time frame for small objects' { $input = @{ Key1 = "Value1"; Key2 = "Value2" } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() ConvertTo-QueryString $input | Out-Null $stopwatch.Stop() $stopwatch.ElapsedMilliseconds | Should -BeLessThan 300 } It 'Should handle large objects efficiently' { $input = @{} for ($i = 1; $i -le 100; $i++) { $input["Key$i"] = "Value$i" } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() ConvertTo-QueryString $input | Out-Null $stopwatch.Stop() $stopwatch.ElapsedMilliseconds | Should -BeLessThan 500 } It 'Should reduce redundant Get-Member calls for objects' { $input = [PSCustomObject]@{ Property1 = "Value1"; Property2 = "Value2" } $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() ConvertTo-QueryString $input | Out-Null $stopwatch.Stop() $stopwatch.ElapsedMilliseconds | Should -BeLessThan 50 } } Context 'Enhanced error handling' { It 'Should handle string input as object with properties' { $input = "string" $result = ConvertTo-QueryString -InputObjects $input # Strings are objects with properties (Length) so they get processed $result | Should -Not -BeNullOrEmpty } It 'Should include supported types information in error message' { $input = 123 try { ConvertTo-QueryString -InputObjects $input -ErrorAction Stop } catch { $_.Exception.Message | Should -Match "Supported types" $_.Exception.Message | Should -Match "Hashtable" $_.Exception.Message | Should -Match "OrderedDictionary" } } It 'Should include the actual input type in error message' { $input = [System.DateTime]::Now try { ConvertTo-QueryString -InputObjects $input -ErrorAction Stop } catch { $_.Exception.Message | Should -Match "System.DateTime" } } It 'Should have correct category and activity in error' { $input = "test" try { ConvertTo-QueryString -InputObjects $input -ErrorAction Stop } catch { $_.CategoryInfo.Category | Should -Be "ParserError" $_.CategoryInfo.Activity | Should -Be "ConvertTo-QueryString" } } It 'Should continue processing after error for unsupported types' { $input = @("string", @{ Key = "Value" }) $results = ConvertTo-QueryString -InputObjects $input $results.Count | Should -Be 2 $results[1] | Should -Be "Key=Value" } } Context 'Real-world Graph request scenarios' { It 'Should handle typical Graph query parameters with null values' { $queryParams = @{ '$select' = 'id,displayName,mail' '$filter' = 'userType eq ''Member''' '$top' = 100 '$skip' = $null '$orderby' = 'displayName' '$count' = $true } $result = ConvertTo-QueryString $queryParams $result | Should -Match '\$select=id%2CdisplayName%2Cmail' $result | Should -Match '\$filter=userType\+eq\+%27Member%27' $result | Should -Match '\$top=100' $result | Should -Match '\$skip=' $result | Should -Match '\$orderby=displayName' $result | Should -Match '\$count=True' } It 'Should handle empty query parameters hashtable' { $queryParams = @{} $result = ConvertTo-QueryString $queryParams $result | Should -Be "" } It 'Should handle query parameters with mixed null and non-null values' { $queryParams = @{ '$select' = 'id,displayName' '$filter' = $null '$top' = 50 '$skip' = $null } $result = ConvertTo-QueryString $queryParams $result | Should -Match '\$select=id%2CdisplayName' $result | Should -Match '\$filter=' $result | Should -Match '\$top=50' $result | Should -Match '\$skip=' } It 'Should handle complex filter scenarios with null values' { $queryParams = @{ '$filter' = 'displayName eq ''John Doe''' '$select' = $null '$expand' = 'manager' '$count' = $null } $result = ConvertTo-QueryString $queryParams $result | Should -Match '\$filter=displayName\+eq\+%27John\+Doe%27' $result | Should -Match '\$select=' $result | Should -Match '\$expand=manager' $result | Should -Match '\$count=' } } Context 'Parameter name encoding' { It 'Should encode parameter names when EncodeParameterNames is specified' { $input = @{ 'test param' = 'value'; 'another¶m' = 'test' } $result = ConvertTo-QueryString -InputObjects $input -EncodeParameterNames $result | Should -Match 'test\+param=value' $result | Should -Match 'another%26param=test' } It 'Should handle null values with encoded parameter names' { $input = @{ 'test param' = $null; 'another¶m' = 'value' } $result = ConvertTo-QueryString -InputObjects $input -EncodeParameterNames $result | Should -Match 'test\+param=' $result | Should -Match 'another%26param=value' } } Context 'ToString conversion' { It 'Should convert integers to string properly' { $input = @{ Number = 42; NullNumber = $null } $result = ConvertTo-QueryString $input $result | Should -Match 'Number=42' $result | Should -Match 'NullNumber=' } It 'Should convert booleans to string properly' { $input = @{ TrueValue = $true; FalseValue = $false; NullBool = $null } $result = ConvertTo-QueryString $input $result | Should -Match 'TrueValue=True' $result | Should -Match 'FalseValue=False' $result | Should -Match 'NullBool=' } It 'Should convert GUIDs to string properly' { $guid = [guid]::NewGuid() $input = @{ GuidValue = $guid; NullGuid = $null } $result = ConvertTo-QueryString $input $result | Should -Match "GuidValue=$($guid.ToString())" $result | Should -Match 'NullGuid=' } It 'Should convert DateTime to string properly' { $date = [System.DateTime]::Now $input = @{ DateValue = $date; NullDate = $null } $result = ConvertTo-QueryString $input $result | Should -Match 'DateValue=' $result | Should -Match 'NullDate=' } } } |