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' $result | Should -Match 'price=99\.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 ([regex]::escape('arrayValue=item1+item2+item3')) $result | Should -Match ([regex]::escape('singleArray=single')) } 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=' } } }  |