ConvertFrom-JsonFast.ms.tests.ps1

import-module pester -RequiredVersion 4.9.0
import-module (Join-Path $PSSCriptRoot "ConvertFrom-JsonFast.psm1") -force

$ErrorActionPreference = "stop"
function New-NestedJson {
    Param(
        [ValidateRange(1, 2048)]
        [int]
        $Depth
    )

    $nestedJson = "true"

    $Depth..1 | ForEach-Object {
        $nestedJson = '{"' + $_ + '":' + $nestedJson + '}'
    }

    return $nestedJson
}

function Count-ObjectDepth {
    Param([PSCustomObject] $InputObject)

    for ($i=1; $i -le 2048; $i++)
    {
        $InputObject = Select-Object -InputObject $InputObject -ExpandProperty $i
        if ($InputObject -eq $true)
        {
            return $i
        }
    }
}

Describe 'ConvertFrom-JsonFast Unit Tests' -tags "CI" {

    BeforeAll {
        $testCasesWithAndWithoutAsHashtableSwitch = @(
            @{ AsHashtable = $true  }
            @{ AsHashtable = $false }
        )
        $testCasesWithAndWithoutNoEnumerateSwitch = @(
            @{ NoEnumerate = $true  }
            @{ NoEnumerate = $false }
        )
    }

    It 'Can convert a single-line object with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable)
        ('{"a" : "1"}' | ConvertFrom-JsonFast -AsHashtable:$AsHashtable).a | Should -Be 1
    }

    It 'Can convert one string-per-object with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable)
        $json = @('{"a" : "1"}', '{"a" : "x"}') | ConvertFrom-JsonFast -AsHashtable:$AsHashtable
        $json.Count | Should -Be 2
        $json[1].a | Should -Be 'x'
        if ($AsHashtable)
        {
            $json | Should -BeOfType Hashtable
        }
    }

    It 'Can convert multi-line object with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable)
        $json = @('{"a" :', '"x"}') | ConvertFrom-JsonFast -AsHashtable:$AsHashtable
        $json.a | Should -Be 'x'
        if ($AsHashtable)
        {
            $json | Should -BeOfType Hashtable
        }
    }

    It 'Can convert an object with Newtonsoft.Json metadata properties with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable)
        $id = 13
        $type = 'Calendar.Months.December'
        $ref = 1989

        $json = '{"$id":' + $id + ', "$type":"' + $type + '", "$ref":' + $ref + '}' | ConvertFrom-JsonFast -AsHashtable:$AsHashtable

        $json.'$id' | Should -Be $id
        $json.'$type' | Should -Be $type
        $json.'$ref' | Should -Be $ref

        if ($AsHashtable)
        {
            $json | Should -BeOfType Hashtable
        }
    }

    It 'Can convert an object of depth 1024 by default with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable)
        $nestedJson = New-NestedJson -Depth 1024

        $json = $nestedJson | ConvertFrom-JsonFast -AsHashtable:$AsHashtable

        if ($AsHashtable)
        {
            $json | Should -BeOfType Hashtable
        }
        else
        {
            $json | Should -BeOfType PSCustomObject
        }
    }

    It 'Fails to convert an object of depth higher than 1024 by default with AsHashtable switch set to <AsHashtable>' -TestCases $testCasesWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable)
        $nestedJson = New-NestedJson -Depth 1025

        { $nestedJson | ConvertFrom-JsonFast -AsHashtable:$AsHashtable } |
            Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand"
    }

    It 'Can correctly round trip arrays with NoEnumerate switch set to <NoEnumerate>' -TestCases $testCasesWithAndWithoutNoEnumerateSwitch {
        Param($NoEnumerate)
        '[ 1, 2 ]' | ConvertFrom-JsonFast -NoEnumerate:$NoEnumerate | ConvertTo-Json -Compress | Should -Be '[1,2]'
    }

    It 'Unravels array elements when NoEnumerate switch is not set' {
        ('[ 1, 2 ]' | ConvertFrom-JsonFast | Measure-Object).Count | Should -Be 2
    }

    It 'Sends a Json array as a single element when NoEnumerate switch is set' {
        ('[ 1, 2 ]' | ConvertFrom-JsonFast -NoEnumerate | Measure-Object).Count | Should -Be 1
    }

    It 'Cannot round trip single element arrays without NoEnumerate switch' {
        '[ 1 ]' | ConvertFrom-JsonFast | ConvertTo-Json | Should -Be 1
    }

    It 'Can round trip single element arrays with NoEnumerate switch' {
        '[ 1 ]' | ConvertFrom-JsonFast -NoEnumerate | ConvertTo-Json -Compress | Should -Be '[1]'
    }

    It 'Can convert null' {
        'null' | ConvertFrom-JsonFast | Should -Be $null
        $out = '[1, null, 2]' | ConvertFrom-JsonFast
        $out.Length | Should -Be 3

        # can't compare directly to array as Pester doesn't handle the $null
        $out[0] | Should -Be 1
        $out[1] | Should -Be $null
        $out[2] | Should -Be 2
    }
}

Describe 'ConvertFrom-JsonFast -Depth Tests' -tags "Feature" {

    BeforeAll {
        $testCasesJsonDepthWithAndWithoutAsHashtableSwitch = @(
            @{ Depth = 2;    AsHashtable = $true  }
            @{ Depth = 2;    AsHashtable = $false }
            @{ Depth = 200;  AsHashtable = $true  }
            @{ Depth = 200;  AsHashtable = $false }
            @{ Depth = 2000; AsHashtable = $true  }
            @{ Depth = 2000; AsHashtable = $false }
        )
    }

    It 'Can convert an object with depth less than Depth param set to <Depth> and AsHashtable switch set to <AsHashtable>' -TestCases $testCasesJsonDepthWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable, $Depth)
        $nestedJson = New-NestedJson -Depth ($Depth - 1)

        $json = $nestedJson | ConvertFrom-JsonFast -AsHashtable:$AsHashtable -Depth $Depth

        if ($AsHashtable)
        {
            $json | Should -BeOfType Hashtable
        }
        else
        {
            $json | Should -BeOfType PSCustomObject
        }

        (Count-ObjectDepth -InputObject $json) | Should -Be ($Depth - 1)
    }

    It 'Can convert an object with depth equal to Depth param set to <Depth> and AsHashtable switch set to <AsHashtable>' -TestCases $testCasesJsonDepthWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable, $Depth)
        $nestedJson = New-NestedJson -Depth:$Depth

        $json = $nestedJson | ConvertFrom-JsonFast -AsHashtable:$AsHashtable -Depth $Depth

        if ($AsHashtable)
        {
            $json | Should -BeOfType Hashtable
        }
        else
        {
            $json | Should -BeOfType PSCustomObject
        }

        (Count-ObjectDepth -InputObject $json) | Should -Be $Depth
    }

    It 'Fails to convert an object with greater depth than Depth param set to <Depth> and AsHashtable switch set to <AsHashtable>' -TestCases $testCasesJsonDepthWithAndWithoutAsHashtableSwitch {
        Param($AsHashtable, $Depth)
        $nestedJson = New-NestedJson -Depth ($Depth + 1)

        { $nestedJson | ConvertFrom-JsonFast -AsHashtable:$AsHashtable -Depth $Depth } |
            Should -Throw -ErrorId "System.ArgumentException,Microsoft.PowerShell.Commands.ConvertFromJsonCommand"
    }
}

Describe "Json Tests" -Tags "Feature" {

    BeforeAll {

        function ValidateSampleObject
        {
            param ($result, [switch]$hasEmbeddedSampleObject )

            Write-Verbose "validating deserialized SampleObject" -Verbose
            $result.SampleInt | Should -Be 98765
            $result.SampleString | Should -Match "stringVal"
            $result.SampleArray.Count | Should -Be 2
            $result.SampleTrue | Should -BeTrue
            $result.SampleFalse | Should -BeFalse
            $result.SampleNull | Should -BeNullOrEmpty
            $result.SampleFloat | Should -Be 9.8765E+43

            if ($hasEmbeddedSampleObject)
            {
                Write-Verbose "validating deserialized Embedded SampleObject" -Verbose
                ValidateSampleObject -result $result.SampleObject
            }
        }

    }

    Context "ConvertTo-Json Bug Fixes" {

        It "ConvertTo-JSON should not have hard coded english error message" {

            # Test follow-up for bug WinBlue: 163372 - ConvertTo-JSON has hard coded english error message.
            $process = Get-Process -Id $PID
            $hash = @{ $process = "def" }
            $expectedFullyQualifiedErrorId = "NonStringKeyInDictionary,Microsoft.PowerShell.Commands.ConvertToJsonCommand"

            { ConvertTo-Json -InputObject $hash } | Should -Throw -ErrorId $expectedFullyQualifiedErrorId
        }

        It "ConvertTo-Json should handle terms with double quotes" {

            # Test follow-up for bug WinBlue: 11484 - ConvertTo-Json can't handle terms with double quotes.

            $notcompressed = ConvertTo-Json @{ FirstName = 'Hello " World' }
            $compressed = ConvertTo-Json @{ FirstName = 'Hello " World' } -Compress
            $valueFromNotCompressedResult = ConvertFrom-Json -InputObject $notcompressed
            $valueFromCompressedResult = ConvertFrom-Json -InputObject $compressed

            $valueFromNotCompressedResult.FirstName | Should -Match $valueFromCompressedResult.FirstName
        }

        It "Convertto-Json should handle Enum based on Int64" {

            # Test follow-up for bug Win8: 378368 Convertto-Json problems with Enum based on Int64.
            if ( $null -eq ("JsonEnumTest" -as "Type")) {
                $enum1 = "TestEnum" + (Get-Random)
                $enum2 = "TestEnum" + (Get-Random)
                $enum3 = "TestEnum" + (Get-Random)

                $jsontype = Add-Type -pass -TypeDef "
                public enum $enum1 : ulong { One = 1, Two = 2 };
                public enum $enum2 : long { One = 1, Two = 2 };
                public enum $enum3 : int { One = 1, Two = 2 };
                public class JsonEnumTest {
                    public $enum1 TestEnum1 = ${enum1}.One;
                    public $enum2 TestEnum2 = ${enum2}.Two;
                    public $enum3 TestEnum3 = ${enum3}.One;
                }"

            }
            $op = [JsonEnumTest]::New() | ConvertTo-Json | ConvertFrom-Json
            $op.TestEnum1 | Should -BeExactly "One"
            $op.TestEnum2 | Should -BeExactly "Two"
            $op.TestEnum3 | Should -Be 1
        }

        It "Test followup for Windows 8 bug 121627" {

            $JsonString = Get-Command Get-help |Select-Object Name, Noun, Verb| ConvertTo-Json
            $actual = ConvertFrom-Json $JsonString

            $actual.Name | Should -BeExactly "Get-Help"
            $actual.Noun | Should -BeExactly "Help"
            $actual.Verb | Should -BeExactly "Get"
        }
    }

    Context "ConvertFrom and ConvertTo on JsonObject Tests" {

        It "Convert dictionary to PSObject" {

            $response = ConvertFrom-Json '{"d":{"__type":"SimpleJsonObject","Name":{"First":"Joel","Last":"Wood"},"Greeting":"Hello"}}'
            $response.d.Name.First | Should -Match "Joel"
        }

        It "Convert to Json using PSObject" -Pending:($IsCoreCLR) {

            $response = ConvertFrom-Json '{"d":{"__type":"SimpleJsonObject","Name":{"First":"Joel","Last":"Wood"},"Greeting":"Hello"}}'

            $response2 = ConvertTo-Json -InputObject $response -ErrorAction Continue
            $response2 = ConvertTo-Json -InputObject $response -ErrorAction Inquire
            $response2 = ConvertTo-Json -InputObject $response -ErrorAction SilentlyContinue
            $response2 = ConvertTo-Json -InputObject $response -Depth 2 -Compress
            $response2 | Should -Be '{"d":{"Name":{"First":"Joel","Last":"Wood"},"Greeting":"Hello"}}'

            $response2 = ConvertTo-Json -InputObject $response -Depth 1 -Compress
            $nameString = [System.Management.Automation.LanguagePrimitives]::ConvertTo($response.d.Name, [string])
            $response2 | Should -BeExactly "{`"d`":{`"Name`":`"$nameString`",`"Greeting`":`"Hello`"}}"

            $result1 = @"
{
    "d": {
              "Name": {
                           "First": "Joel",
                           "Last": "Wood"
                       },
              "Greeting": "Hello"
          }
}
"@

            $response2 = ConvertTo-Json -InputObject $response -Depth 2
            $response2 | Should -Match $result1

            $result2 = @"
{
    "d": {
              "Name": "$nameString",
              "Greeting": "Hello"
          }
}
"@

            $response2 = ConvertTo-Json -InputObject $response -Depth 1
            $response2 | Should -Match $result2

            $arraylist = New-Object System.Collections.ArrayList
            [void]$arraylist.Add("one")
            [void]$arraylist.Add("two")
            [void]$arraylist.Add("three")
            $response2 = ConvertTo-Json -InputObject $arraylist -Compress
            $response2 | Should -Be '["one","two","three"]'

            $result3 = @"
[
    "one",
    "two",
    "three"
]
"@

            $response2 = ConvertTo-Json -InputObject $arraylist
            $response2 | Should -Be $result3

            $response2 = $arraylist | ConvertTo-Json
            $response2 | Should -Be $result3
        }

        It "Convert to Json using hashtable" -Pending:($IsCoreCLR) {

            $nameHash = @{First="Joe1";Last="Wood"}
            $dHash = @{Name=$nameHash; Greeting="Hello"}
            $rootHash = @{d=$dHash}
            $response3 = ConvertTo-Json -InputObject $rootHash -Depth 2 -Compress
            $response3 | Should -Be '{"d":{"Greeting":"Hello","Name":{"Last":"Wood","First":"Joe1"}}}'

            $response3 = ConvertTo-Json -InputObject $rootHash -Depth 1 -Compress
            $response3 | Should -Be '{"d":{"Greeting":"Hello","Name":"System.Collections.Hashtable"}}'

            $result4 = @"
{
    "d": {
              "Greeting": "Hello",
              "Name": {
                           "Last": "Wood",
                           "First": "Joe1"
                       }
          }
}
"@

            $response3 = ConvertTo-Json -InputObject $rootHash -Depth 2
            $response3 | Should -Be $result4

            $result5 = @"
{
    "d": {
              "Greeting": "Hello",
              "Name": "System.Collections.Hashtable"
          }
}
"@

            $response3 = ConvertTo-Json -InputObject $rootHash -Depth 1
            $response3 | Should -Be $result5
        }

        It "Convert from Json allows an empty string" {

            $emptyStringResult = ConvertFrom-Json ""
            $emptyStringResult | Should -BeNullOrEmpty
        }

        It "Convert enumerated values to Json" {

            $sampleObject = [pscustomobject]@{
                PSTypeName = 'Test.EnumSample'
                SampleSimpleEnum = [System.Management.Automation.ActionPreference]::Ignore
                SampleBitwiseEnum = [System.Management.Automation.CommandTypes]'Alias,Function,Cmdlet'
            }

            $response4 = ConvertTo-Json -InputObject $sampleObject -Compress
            $response4 | Should -Be '{"SampleSimpleEnum":4,"SampleBitwiseEnum":11}'

            $response4 = ConvertTo-Json -InputObject $sampleObject -Compress -EnumsAsStrings
            $response4 | Should -Be '{"SampleSimpleEnum":"Ignore","SampleBitwiseEnum":"Alias, Function, Cmdlet"}'

        }

    }

    Context "JsonObject Tests" {

        It "AddMember on JsonObject" {

            # create a Version object
            $versionObject = New-Object System.Version 2, 3, 4, 14

            # add a NoteProperty member called Note with a text note
            $versionObject | Add-Member -MemberType NoteProperty -Name Note -Value "a version object"

            # add an AliasProperty called Rev as an alias to the Revision property
            $versionObject | Add-Member -MemberType AliasProperty -Name Rev -Value Revision

            # add a ScriptProperty called IsOld which returns whether the version is an older version
            $versionObject | Add-Member -MemberType ScriptProperty -Name IsOld -Value { ($this.Major -le 3) }
            $jstr = ConvertTo-Json $versionObject

            # convert the JSON string to a JSON object
            $json = ConvertFrom-Json $jstr

            # Check the basic properties
            $json.Major | Should -Be 2
            $json.Minor | Should -Be 3
            $json.Build | Should -Be 4
            $json.Revision | Should -Be 14
            $json.Note | Should -Match "a version object"

            # Check the AliasProperty
            $json.Rev | Should -Be $json.Revision

            # Check the ScriptProperty
            $json.IsOld | Should -BeTrue
        }

        It "ConvertFrom-Json with a key value pair" {

            $json = "{name:1}"
            $result = ConvertFrom-Json $json
            $result.name | Should -Be 1
        }

        It "ConvertFrom-Json with a simple array" {

            $json = "[1,2,3,4,5,6]"
            $result = ConvertFrom-Json $json
            $result.Count | Should -Be 6
            ,$result | Should -BeOfType System.Array
        }

        It "ConvertFrom-Json with a float value" {

            $json = '{"SampleFloat1":1.2345E67, "SampleFloat2":-7.6543E-12}'
            $result = ConvertFrom-Json $json

            $sampleFloat1 = Invoke-Expression 1.2345E67
            $result.SampleFloat1 | Should -Be $sampleFloat1

            $sampleFloat2 = Invoke-Expression -7.6543E-12
            $result.SampleFloat2 | Should -Be $sampleFloat2
        }

        It "ConvertFrom-Json hash table nested in array" {

            $json = "['one', 'two', {'First':1,'Second':2,'Third':['Five','Six', 'Seven']}, 'four']"
            $result = ConvertFrom-Json $json

            $result.Count | Should -Be 4
            $result[0] | Should -BeExactly "one"
            $result[1] | Should -BeExactly "two"
            $result[3] | Should -BeExactly "four"

            $hash = $result[2]
            $hash.First | Should -Be 1
            $hash.Second | Should -Be 2
            $hash.Third.Count | Should -Be 3
            $hash.Third[0] | Should -BeExactly "Five"
            $hash.Third[1] | Should -BeExactly "Six"
            $hash.Third[2] | Should -BeExactly "Seven"
        }

        It "ConvertFrom-Json array nested in hash table" {

            $json = '{"First":["one", "two", "three"], "Second":["four", "five"], "Third": {"blah": 4}}'
            $result = ConvertFrom-Json $json

            $result.First.Count | Should -Be 3
            $result.First[0] | Should -BeExactly "one"
            $result.First[1] | Should -BeExactly "two"
            $result.First[2] | Should -BeExactly "three"

            $result.Second.Count | Should -Be 2
            $result.Second[0] | Should -BeExactly "four"
            $result.Second[1] | Should -BeExactly "five"

            $result.Third.blah | Should -BeExactly "4"
        }

        It "ConvertFrom-Json case insensitive test" {

            $json = '{"sAMPleValUE":12345}'
            $result = ConvertFrom-Json $json

            $result.SampleValue | Should -Be 12345
        }

        It "ConvertFrom-Json sample values" {

            $json = '{"SampleInt":98765, "SampleString":"stringVal","SampleArray":[2,"two"], "SampleTrue":true, "SampleFalse":false,"SampleNull":null, "SampleFloat":9.8765E43}'
            $result = ConvertFrom-Json $json

            # Validate the result object
            ValidateSampleObject -result $result

            $json = '{"SampleInt":98765, "SampleString":"stringVal","SampleArray":[2,"two"], "SampleTrue":true, ' +
                    '"SampleFalse":false,"SampleNull":null, "SampleFloat":9.8765E43, "SampleObject":'+
                    '{"SampleInt":98765, "SampleString":"stringVal","SampleArray":[2,"two"], '+
                    '"SampleTrue":true, "SampleFalse":false,"SampleNull":null, "SampleFloat":9.8765E43}}'

            # Validate the result object
            $result = ConvertFrom-Json $json
            ValidateSampleObject -result $result -hasEmbeddedSampleObject
        }

        It "ConvertFrom-Json with special characters" {

            $json = '{"SampleValue":"\"\\\b\f\n\r\t\u4321\uD7FF"}'
            $result = ConvertFrom-Json $json
            $result.SampleValue[0] | Should -Be '"'
            $result.SampleValue[1] | Should -Be '\'
            $result.SampleValue[2] | Should -Be 0x8
            $result.SampleValue[3] | Should -Be 0xC
            $result.SampleValue[4] | Should -Be 0xA
            $result.SampleValue[5] | Should -Be 0xD
            $result.SampleValue[6] | Should -Be 0x9
            $result.SampleValue[7] | Should -Be 0x4321
            $result.SampleValue[8] | Should -Be 0xD7FF
        }
    }
}

# This Describe is for new Json tests
#
Describe "Validate Json serialization" -Tags "CI" {

    Context "Validate Json serialization ascii values" {

        $testCases = @(
            @{
                TestInput = 0
                ToJson = '"\u0000"'
                FromJson = ''
             }
            @{
                TestInput = 1
                ToJson = '"\u0001"'
                FromJson = ''
             }
            @{
                TestInput = 2
                ToJson = '"\u0002"'
                FromJson = ''
             }
            @{
                TestInput = 3
                ToJson = '"\u0003"'
                FromJson = ''
             }
            @{
                TestInput = 4
                ToJson = '"\u0004"'
                FromJson = ''
             }
            @{
                TestInput = 5
                ToJson = '"\u0005"'
                FromJson = ''
             }
            @{
                TestInput = 6
                ToJson = '"\u0006"'
                FromJson = ''
             }
            @{
                TestInput = 7
                ToJson = '"\u0007"'
                FromJson = ''
             }
            @{
                TestInput = 8
                ToJson = '"\b"'
                FromJson = ''
             }
            @{
                TestInput = 9
                ToJson = '"\t"'
                FromJson = ' '
             }
            @{
                TestInput = 10
                ToJson = '"\n"'
                FromJson = "`n"
             }
            @{
                TestInput = 11
                ToJson = '"\u000b"'
                FromJson = ' '
             }
            @{
                TestInput = 12
                ToJson = '"\f"'
                FromJson = ' '
             }
            @{
                TestInput = 13
                ToJson = '"\r"'
                FromJson = ''
             }
            @{
                TestInput = 14
                ToJson = '"\u000e"'
                FromJson = ''
             }
            @{
                TestInput = 15
                ToJson = '"\u000f"'
                FromJson = ''
             }
            @{
                TestInput = 16
                ToJson = '"\u0010"'
                FromJson = ''
             }
            @{
                TestInput = 17
                ToJson = '"\u0011"'
                FromJson = ''
             }
            @{
                TestInput = 18
                ToJson = '"\u0012"'
                FromJson = ''
             }
            @{
                TestInput = 19
                ToJson = '"\u0013"'
                FromJson = ''
             }
            @{
                TestInput = 20
                ToJson = '"\u0014"'
                FromJson = ''
             }
            @{
                TestInput = 21
                ToJson = '"\u0015"'
                FromJson = ''
             }
            @{
                TestInput = 22
                ToJson = '"\u0016"'
                FromJson = ''
             }
            @{
                TestInput = 23
                ToJson = '"\u0017"'
                FromJson = ''
             }
            @{
                TestInput = 24
                ToJson = '"\u0018"'
                FromJson = ''
             }
            @{
                TestInput = 25
                ToJson = '"\u0019"'
                FromJson = ''
             }
            @{
                TestInput = 26
                ToJson = '"\u001a"'
                FromJson = ''
             }
            @{
                TestInput = 27
                ToJson = '"\u001b"'
                FromJson = ''
             }
            @{
                TestInput = 28
                ToJson = '"\u001c"'
                FromJson = ''
             }
            @{
                TestInput = 29
                ToJson = '"\u001d"'
                FromJson = ''
             }
            @{
                TestInput = 30
                ToJson = '"\u001e"'
                FromJson = ''
             }
            @{
                TestInput = 31
                ToJson = '"\u001f"'
                FromJson = ''
             }
            @{
                TestInput = 32
                ToJson = '" "'
                FromJson = ' '
             }
            @{
                TestInput = 33
                ToJson = '"!"'
                FromJson = '!'
             }
            @{
                TestInput = 34
                ToJson = '"\""'
                FromJson = '"'
             }
            @{
                TestInput = 35
                ToJson = '"#"'
                FromJson = '#'
             }
            @{
                TestInput = 36
                ToJson = '"$"'
                FromJson = '$'
             }
            @{
                TestInput = 37
                ToJson = '"%"'
                FromJson = '%'
             }
            @{
                TestInput = 38
                ToJson = if ( $IsCoreCLR ) { '"&"' } else { '"\u0026"' }
                FromJson = '&'
             }
            @{
                TestInput = 39
                ToJson = if ( $IsCoreCLR ) { '"''"' } else { '"\u0027"' }
                FromJson = "'"
             }
            @{
                TestInput = 40
                ToJson = '"("'
                FromJson = '('
             }
            @{
                TestInput = 41
                ToJson = '")"'
                FromJson = ')'
             }
            @{
                TestInput = 42
                ToJson = '"*"'
                FromJson = '*'
             }
            @{
                TestInput = 43
                ToJson = '"+"'
                FromJson = '+'
             }
            @{
                TestInput = 44
                ToJson = '","'
                FromJson = ','
             }
            @{
                TestInput = 45
                ToJson = '"-"'
                FromJson = '-'
             }
            @{
                TestInput = 46
                ToJson = '"."'
                FromJson = '.'
             }
            @{
                TestInput = 47
                ToJson = '"/"'
                FromJson = '/'
             }
            @{
                TestInput = 48
                ToJson = '"0"'
                FromJson = '0'
             }
            @{
                TestInput = 49
                ToJson = '"1"'
                FromJson = '1'
             }
            @{
                TestInput = 50
                ToJson = '"2"'
                FromJson = '2'
             }
            @{
                TestInput = 51
                ToJson = '"3"'
                FromJson = '3'
             }
            @{
                TestInput = 52
                ToJson = '"4"'
                FromJson = '4'
             }
            @{
                TestInput = 53
                ToJson = '"5"'
                FromJson = '5'
             }
            @{
                TestInput = 54
                ToJson = '"6"'
                FromJson = '6'
             }
            @{
                TestInput = 55
                ToJson = '"7"'
                FromJson = '7'
             }
            @{
                TestInput = 56
                ToJson = '"8"'
                FromJson = '8'
             }
            @{
                TestInput = 57
                ToJson = '"9"'
                FromJson = '9'
             }
            @{
                TestInput = 58
                ToJson = '":"'
                FromJson = ':'
             }
            @{
                TestInput = 59
                ToJson = '";"'
                FromJson = ';'
             }
            @{
                TestInput = 60
                ToJson = if ( $IsCoreCLR ) { '"<"' } else { '"\u003c"' }
                FromJson = '<'
             }
            @{
                TestInput = 61
                ToJson = '"="'
                FromJson = '='
             }
            @{
                TestInput = 62
                ToJson = if ( $IsCoreCLR ) { '">"' } else { '"\u003e"' }
                FromJson = '>'
             }
            @{
                TestInput = 63
                ToJson = '"?"'
                FromJson = '?'
             }
            @{
                TestInput = 64
                ToJson = '"@"'
                FromJson = '@'
             }
            @{
                TestInput = 65
                ToJson = '"A"'
                FromJson = 'A'
             }
            @{
                TestInput = 66
                ToJson = '"B"'
                FromJson = 'B'
             }
            @{
                TestInput = 67
                ToJson = '"C"'
                FromJson = 'C'
             }
            @{
                TestInput = 68
                ToJson = '"D"'
                FromJson = 'D'
             }
            @{
                TestInput = 69
                ToJson = '"E"'
                FromJson = 'E'
             }
            @{
                TestInput = 70
                ToJson = '"F"'
                FromJson = 'F'
             }
            @{
                TestInput = 71
                ToJson = '"G"'
                FromJson = 'G'
             }
            @{
                TestInput = 72
                ToJson = '"H"'
                FromJson = 'H'
             }
            @{
                TestInput = 73
                ToJson = '"I"'
                FromJson = 'I'
             }
            @{
                TestInput = 74
                ToJson = '"J"'
                FromJson = 'J'
             }
            @{
                TestInput = 75
                ToJson = '"K"'
                FromJson = 'K'
             }
            @{
                TestInput = 76
                ToJson = '"L"'
                FromJson = 'L'
             }
            @{
                TestInput = 77
                ToJson = '"M"'
                FromJson = 'M'
             }
            @{
                TestInput = 78
                ToJson = '"N"'
                FromJson = 'N'
             }
            @{
                TestInput = 79
                ToJson = '"O"'
                FromJson = 'O'
             }
            @{
                TestInput = 80
                ToJson = '"P"'
                FromJson = 'P'
             }
            @{
                TestInput = 81
                ToJson = '"Q"'
                FromJson = 'Q'
             }
            @{
                TestInput = 82
                ToJson = '"R"'
                FromJson = 'R'
             }
            @{
                TestInput = 83
                ToJson = '"S"'
                FromJson = 'S'
             }
            @{
                TestInput = 84
                ToJson = '"T"'
                FromJson = 'T'
             }
            @{
                TestInput = 85
                ToJson = '"U"'
                FromJson = 'U'
             }
            @{
                TestInput = 86
                ToJson = '"V"'
                FromJson = 'V'
             }
            @{
                TestInput = 87
                ToJson = '"W"'
                FromJson = 'W'
             }
            @{
                TestInput = 88
                ToJson = '"X"'
                FromJson = 'X'
             }
            @{
                TestInput = 89
                ToJson = '"Y"'
                FromJson = 'Y'
             }
            @{
                TestInput = 90
                ToJson = '"Z"'
                FromJson = 'Z'
             }
            @{
                TestInput = 91
                ToJson = '"["'
                FromJson = '['
             }
            @{
                TestInput = 92
                ToJson = '"\\"'
                FromJson = '\'
             }
            @{
                TestInput = 93
                ToJson = '"]"'
                FromJson = ']'
             }
            @{
                TestInput = 94
                ToJson = '"^"'
                FromJson = '^'
             }
            @{
                TestInput = 95
                ToJson = '"_"'
                FromJson = '_'
             }
            @{
                TestInput = 96
                ToJson = '"`"'
                FromJson = '`'
             }
            @{
                TestInput = 97
                ToJson = '"a"'
                FromJson = 'a'
             }
            @{
                TestInput = 98
                ToJson = '"b"'
                FromJson = 'b'
             }
            @{
                TestInput = 99
                ToJson = '"c"'
                FromJson = 'c'
             }
            @{
                TestInput = 100
                ToJson = '"d"'
                FromJson = 'd'
             }
            @{
                TestInput = 101
                ToJson = '"e"'
                FromJson = 'e'
             }
            @{
                TestInput = 102
                ToJson = '"f"'
                FromJson = 'f'
             }
            @{
                TestInput = 103
                ToJson = '"g"'
                FromJson = 'g'
             }
            @{
                TestInput = 104
                ToJson = '"h"'
                FromJson = 'h'
             }
            @{
                TestInput = 105
                ToJson = '"i"'
                FromJson = 'i'
             }
            @{
                TestInput = 106
                ToJson = '"j"'
                FromJson = 'j'
             }
            @{
                TestInput = 107
                ToJson = '"k"'
                FromJson = 'k'
             }
            @{
                TestInput = 108
                ToJson = '"l"'
                FromJson = 'l'
             }
            @{
                TestInput = 109
                ToJson = '"m"'
                FromJson = 'm'
             }
            @{
                TestInput = 110
                ToJson = '"n"'
                FromJson = 'n'
             }
            @{
                TestInput = 111
                ToJson = '"o"'
                FromJson = 'o'
             }
            @{
                TestInput = 112
                ToJson = '"p"'
                FromJson = 'p'
             }
            @{
                TestInput = 113
                ToJson = '"q"'
                FromJson = 'q'
             }
            @{
                TestInput = 114
                ToJson = '"r"'
                FromJson = 'r'
             }
            @{
                TestInput = 115
                ToJson = '"s"'
                FromJson = 's'
             }
            @{
                TestInput = 116
                ToJson = '"t"'
                FromJson = 't'
             }
            @{
                TestInput = 117
                ToJson = '"u"'
                FromJson = 'u'
             }
            @{
                TestInput = 118
                ToJson = '"v"'
                FromJson = 'v'
             }
            @{
                TestInput = 119
                ToJson = '"w"'
                FromJson = 'w'
             }
            @{
                TestInput = 120
                ToJson = '"x"'
                FromJson = 'x'
             }
            @{
                TestInput = 121
                ToJson = '"y"'
                FromJson = 'y'
             }
            @{
                TestInput = 122
                ToJson = '"z"'
                FromJson = 'z'
             }
            @{
                TestInput = 123
                ToJson = '"{"'
                FromJson = '{'
             }
            @{
                TestInput = 124
                ToJson = '"|"'
                FromJson = '|'
             }
            @{
                TestInput = 125
                ToJson = '"}"'
                FromJson = '}'
             }
            @{
                TestInput = 126
                ToJson = '"~"'
                FromJson = '~'
             }
            @{
                TestInput = 127
                ToJson = '""'
                FromJson = ''
             }
        )


        It "Validate 'ConvertTo-Json ([char]$($testCase.TestInput))', and 'ConvertTo-Json ([char]$($testCase.TestInput)) | ConvertFrom-Json'" -TestCases $TestCases {
            param (
                $TestInput,
                $ToJson,
                $FromJson
            )
            
            $result = @{
                ToJson = ConvertTo-Json ([char]$TestInput)
                FromJson = ConvertTo-Json ([char]$TestInput) | ConvertFrom-Json
            }

            if ($FromJson)
            {
                $result.FromJson | Should -Be $FromJson
            }
            else
            {
                # There are two char for which the deserialized object must be compare to the serialized one via "Should Match"
                # These values are [char]0 and [char]13.
                $result.FromJson | Should -Match $FromJson
            }
            $result.ToJson | Should -Be $ToJson
        }
        
    }

    Context "Validate Json serialization for types" {

        $testCases = @(

            ## Decimal types - Decimals are a 128-bit data type
            @{
                TestInput = '[decimal]::MinValue'
                FromJson = [decimal]::MinValue
                ToJson = [decimal]::MinValue
            }
            @{
                TestInput = '[decimal]::MaxValue'
                FromJson = [decimal]::MaxValue
                ToJson = [decimal]::MaxValue
            }

            # An sbyte is a signed 8-bit integer, and it ranges from -128 to 127.
            # A byte is an unsigned 8-bit integer that ranges from 0 to 255
            @{
                TestInput = '[byte]::MinValue'
                FromJson = [byte]::MinValue
                ToJson = [byte]::MinValue
            }
            @{
                TestInput = '[byte]::MaxValue'
                FromJson = [byte]::MaxValue
                ToJson = [byte]::MaxValue
            }
            @{
                TestInput = '[sbyte]::MinValue'
                FromJson = [sbyte]::MinValue
                ToJson = [sbyte]::MinValue
            }
            @{
                TestInput = '[sbyte]::MaxValue'
                FromJson = [sbyte]::MaxValue
                ToJson = [sbyte]::MaxValue
            }
            @{
                TestInput = '[char]::MinValue'
                FromJson = $null
                ToJson = 'null'
            }
            @{
                TestInput = '[char]::MaxValue - 1'
                FromJson = [char]::MaxValue - 1
                ToJson = [char]::MaxValue - 1
            }
            @{
                TestInput = '[string]::Empty'
                FromJson = [string]::Empty
                ToJson = '""'
            }
            @{
                TestInput = '[string]"hello"'
                FromJson = [string]"hello"
                ToJson = '"hello"'
            }

            # Int, int32, uint32, uint16, int16

            # 32-bit signed integer
            @{
                TestInput = '[int]::MaxValue'
                FromJson = [int]::MaxValue
                ToJson = [int]::MaxValue
            }
            @{
                TestInput = '[int]::MinValue'
                FromJson = [int]::MinValue
                ToJson = [int]::MinValue
            }
            @{
                TestInput = '[int32]::MaxValue'
                FromJson = [int32]::MaxValue
                ToJson = [int32]::MaxValue
            }
            @{
                TestInput = '[int32]::MinValue'
                FromJson = [int32]::MinValue
                ToJson = [int32]::MinValue
            }

            # 32-bit unsigned integer
            @{
                TestInput = '[uint32]::MaxValue'
                FromJson = [uint32]::MaxValue
                ToJson = [uint32]::MaxValue
            }
            @{
                TestInput = '[uint32]::MinValue'
                FromJson = [uint32]::MinValue
                ToJson = [uint32]::MinValue
            }

            # 16-bit unsigned integer
            @{
                TestInput = '[int16]::MinValue'
                FromJson = [int16]::MinValue
                ToJson = [int16]::MinValue
            }
            @{
                TestInput = '[uint16]::MaxValue'
                FromJson = [uint16]::MaxValue
                ToJson = [uint16]::MaxValue
            }

            # 64-bit unsigned integer
            @{
                TestInput = '[uint64]::MinValue'
                FromJson = [uint64]::MinValue
                ToJson = [uint64]::MinValue
            }
            @{
                TestInput = '[uint64]::MinValue'
                FromJson = [uint64]::MinValue
                ToJson = [uint64]::MinValue
            }

            # 64 bit signed integer
            @{
                TestInput = '[int64]::MaxValue'
                FromJson = [int64]::MaxValue
                ToJson = [int64]::MaxValue
            }
            @{
                TestInput = '[int64]::MinValue'
                FromJson = [int64]::MinValue
                ToJson = [int64]::MinValue
            }
            @{
                TestInput = '[long]::MaxValue'
                FromJson = [long]::MaxValue
                ToJson = [long]::MaxValue
            }
            @{
                TestInput = '[long]::MinValue'
                FromJson = [long]::MinValue
                ToJson = [long]::MinValue
            }

            # Bool
            @{
                TestInput = '[bool](1)'
                FromJson = [bool](1)
                ToJson = $true
            }
            @{
                TestInput = '[bool](0)'
                FromJson = $false
                ToJson = 'False'
            }

            # Decimal
            @{
                TestInput = '[decimal]::MaxValue'
                FromJson = [decimal]::MaxValue
                ToJson = [decimal]::MaxValue
            }
            @{
                TestInput = '[decimal]::MinValue'
                FromJson = [decimal]::MinValue
                ToJson = [decimal]::MinValue
            }

            # Single
            @{
                TestInput = '[single]::MaxValue'
                FromJson = "3.4028235E+38"
                ToJson = "3.4028235E+38"
            }
            @{
                TestInput = '[single]::MinValue'
                FromJson = "-3.4028235E+38"
                ToJson = "-3.4028235E+38"
            }

            # Double
            @{
                TestInput = '[double]::MaxValue'
                FromJson = [double]::MaxValue
                ToJson = [double]::MaxValue
            }
            @{
                TestInput = '[double]::MinValue'
                FromJson = [double]::MinValue
                ToJson = [double]::MinValue
            }
        )

        function ValidateJsonSerialization
        {
            param ($testCase)

            if ( $TestCase.TestInput -eq "[char]::MinValue" ) { $pending = $true } else { $pending = $false }
            It "Validate '$($testCase.TestInput) | ConvertTo-Json' and '$($testCase.TestInput) | ConvertTo-Json | ConvertFrom-Json'" -Pending:$pending {

                # The test case input is executed via invoke-expression. Then, we use this value as an input to ConvertTo-Json,
                # and the result is saved into in the $result.ToJson variable. Lastly, this value is deserialized back using
                # ConvertFrom-Json, and the value is saved to $result.FromJson for comparison.

                $expression = Invoke-Expression $testCase.TestInput
                $result = @{
                    ToJson = $expression | ConvertTo-Json
                    FromJson = $expression | ConvertTo-Json | ConvertFrom-Json
                }

                $result.ToJson | Should -Be $testCase.ToJson
                $result.FromJson | Should -Be $testCase.FromJson
            }
        }

        foreach ($testCase in $testCases)
        {
            ValidateJsonSerialization $testCase
        }
    }

    Context "Validate Json Serialization for 'Get-CimClass' and 'Get-Command'" {

        function ValidateProperties
        {
            param (
                $serialized,
                $expected,
                $properties
            )

            # Validate that the two collections are the same size.
            $expected.Count | Should -Be $serialized.Count

            for ($index = 0; $index -lt $serialized.Count; $index++)
            {
                $serializedObject = $serialized[$index]
                $expectedObject = $expected[$index]
                foreach ($property in $properties)
                {
                    # Write-Verbose "Validating $property" -Verbose
                    if ($property -eq "Qualifiers")
                    {
                        $serializedObject.$property.Count | Should -Be $expectedObject.$property.Count
                    }
                    else
                    {
                        $serializedObject.$property | Should -Be $expectedObject.$property
                    }
                }
            }
        }

        It "Validate that CimClass Properties for win32_bios can be serialized using ConvertTo-Json and ConvertFrom-Json" -Skip {

            $class = Get-CimClass win32_bios

            $result = @{
                Expected = $class.CimClassProperties | ForEach-Object {$_}
                SerializedViaJson = $class.CimClassProperties | ConvertTo-Json -Depth 10 | ConvertFrom-Json
            }

            $propertiesToValidate = @("Name", "Flags", "Qualifiers", "ReferenceClassName")

            ValidateProperties -serialized $result.SerializedViaJson -expected $result.Expected -properties $propertiesToValidate
        }

        It "Validate 'Get-Command Get-help' output with Json conversion" {

            $result = @{
                Expected = @(Get-Command Get-help)
                SerializedViaJson = @(Get-Command Get-help | ConvertTo-Json | ConvertFrom-Json)
            }

            $propertiesToValidate = @("Name", "Noun", "Verb")
            ValidateProperties -serialized $result.SerializedViaJson -expected $result.Expected -properties $propertiesToValidate
        }

        It "Validate 'Get-Command Get-Help, Get-command, Get-Member' output with Json conversion" {

            $result = @{
                Expected = @(Get-Command Get-Help, Get-Command, Get-Member)
                SerializedViaJson = @(Get-Command Get-Help, Get-Command, Get-Member) | ConvertTo-Json | ConvertFrom-Json
            }

            $propertiesToValidate = @("Name", "Source", "HelpFile")
            ValidateProperties -serialized $result.SerializedViaJson -expected $result.Expected -properties $propertiesToValidate
        }

        It "ConvertTo-JSON a dictionary of arrays" {
            $a = 1..5
            $b = 6..10

            # remove whitespace (and newline/cr) which reduces the complexity of a
            # cross-plat test
            $actual = ([ordered]@{'a'=$a;'b'=$b} | ConvertTo-Json) -replace "\s"
            $expected = @'
{
    "a": [
              1,
              2,
              3,
              4,
              5
          ],
    "b": [
              6,
              7,
              8,
              9,
              10
          ]
}
'@

            $expectedNoWhiteSpace = $expected -replace "\s"
            $actual | Should -Be $expectedNoWhiteSpace
        }
    }

    Context "Validate Json output is either Pretty or Compressed" {

        It "Should print a pretty Array" {
            $array = 'one', 'two', 'three'
            $response = $array | ConvertTo-Json
            ($response -split "\r?\n")[1] | Should -Be ' "one",'
        }

        It "Should print a pretty dictionary" {
            $dictionary = [Ordered]@{
                'one' = 1
                'two' = 2
                'three' = 3
            }
            $response2 = $dictionary | ConvertTo-Json
            ($response2 -split "\r?\n")[1] | Should -Be ' "one": 1,'
        }

        It "Should minify Json with Compress switch" {
            (@{ a = 1 } | ConvertTo-Json -Compress).Length | Should -Be 7
        }
    }
}

Describe "Json Bug fixes"  -Tags "Feature" {

    function RunJsonTest
    {
        param ($testCase)

        It "$($testCase.Name)" {

            # Create a nested object
            $start = 1
            $previous = @{
                Depth = $($testCase.NumberOfElements)
                Next = $null
            }

            ($($testCase.NumberOfElements)-1)..$start | ForEach-Object {
                $current = @{
                    Depth = $_
                    Next = $previous
                }
                $previous = $current
            }

            if ($testCase.ShouldThrow)
            {
                { $previous | ConvertTo-Json -Depth $testCase.MaxDepth } | Should -Throw -ErrorId $testCase.FullyQualifiedErrorId
            }
            else
            {
                   { $previous | ConvertTo-Json -Depth $testCase.MaxDepth | ConvertFrom-Json } | Should -Not -Throw
            }
        }
    }

    $testCases = @(
        @{
            Name = "ConvertTo-Json -Depth 101 throws MaximumAllowedDepthReached when the user specifies a depth greater than 100."
            NumberOfElements = 10
            MaxDepth = 101
            FullyQualifiedErrorId = "ReachedMaximumDepthAllowed,Microsoft.PowerShell.Commands.ConvertToJsonCommand"
            ShouldThrow = $true
        }
        @{
            Name = "ConvertTo-Json and ConvertFrom-Json work for any depth less than or equal to 100."
            NumberOfElements = 100
            MaxDepth = 100
            ShouldThrow = $false
        }
        @{
            Name = "ConvertTo-Json and ConvertFrom-Json work for depth 100 with an object larger than 100."
            NumberOfElements = 105
            MaxDepth = 100
            ShouldThrow = $false
        }
    )

    foreach ($testCase in $testCases)
    {
        RunJsonTest $testCase
    }

    It "ConvertFrom-Json deserializes an array of PSObjects (in multiple lines) as a single string." {

        # Create an array of PSCustomObjects, and serialize it
        $array = [pscustomobject]@{ objectName = "object1Name"; objectValue = "object1Value" },
                 [pscustomobject]@{ objectName = "object2Name"; objectValue = "object2Value" }

        # Serialize the array to a text file
        $filePath = Join-Path $TESTDRIVE test.json
        $array | ConvertTo-Json | Out-File $filePath -Encoding utf8

        # Read the object as an array of PSObjects and deserialize it.
        $result = Get-Content $filePath | ConvertFrom-Json
        $result.Count | Should -Be 2
    }

    It "ConvertFrom-Json deserializes an array of strings (in multiple lines) as a single string." {

        $result = "[1,","2,","3]" | ConvertFrom-Json
        $result.Count | Should -Be 3
    }

    It 'ConvertTo-Json will output warning if depth is exceeded.' {
        $a = @{ a = @{ b = @{ c = @{ d = 1 } } } }
        $json = $a | ConvertTo-Json -Depth 2 -WarningVariable warningMessage -WarningAction SilentlyContinue
        $json | Should -Not -BeNullOrEmpty
        $warningMessage | Should -Not -BeNullOrEmpty
    }
}