Tests/PSNode.Tests.ps1

#Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"}

using module ..\..\ObjectGraphTools

[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')]
[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Node',   Justification = 'False positive')]
param()

Describe 'PSNode' {

    BeforeAll {
        Set-StrictMode -Version Latest

        $Object = @{
            String = 'Hello World'
            Array = @(
                @{ Index = 1; Name = 'One';   Comment = 'First item'  }
                @{ Index = 2; Name = 'Two';   Comment = 'Second item' }
                @{ Index = 3; Name = 'Three'; Comment = 'Third item'  }
            )
            HashTable = @{ One = 1; Two = 2; Three = 3 }
            PSCustomObject = [PSCustomObject]@{ One = 1; Two = 2; Three = 3 }
        }

        function Iterate([PSNode]$Node) { # Basic iterator
            $Node.Path
            if ($Node -is [PSCollectionNode]) {
                $Node.ChildNodes.foreach{ Iterate $_ }
            }
        }
    }

    Context 'Existence Check' {

        It 'Loaded' {
            [PSNode]::new() -is [PSNode] | Should -BeTrue
        }
    }

    Context 'ParseInput' {

        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'PSNode' {
            $Node -is [PSNode] | Should -BeTrue
        }

        it "Value" {
            $Node.Value | Should -Be $Object
        }

        it "ValueType" {
            $Node.ValueType | Should -Be 'HashTable'
            { $Node.ValueType = 'Something' } | Should -Throw '*ReadOnly*' -ExceptionType System.Management.Automation.RuntimeException
        }
    }

    Context 'Get child node' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'String' {
            $ItemNode = $Node.GetChildNode('String')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.String
            $ItemNode.ValueType | Should -Be $Object.String.GetType()
        }

        it 'Array' {
            $ItemNode = $Node.GetChildNode('Array')
            $ItemNode -is [PSNode]           | Should -BeTrue
            $ItemNode -is [PSCollectionNode] | Should -BeTrue
            $ItemNode -is [PSListNode]       | Should -BeTrue
            $ItemNode.ValueType | Should -Be $Object.Array.GetType()
        }

        it 'HashTable' {
            $ItemNode = $Node.GetChildNode('HashTable')
            $ItemNode -is [PSNode]           | Should -BeTrue
            $ItemNode -is [PSCollectionNode] | Should -BeTrue
            $ItemNode -is [PSMapNode]        | Should -BeTrue
            $ItemNode -is [PSDictionaryNode] | Should -BeTrue
            $ItemNode.ValueType | Should -Be $Object.HashTable.GetType()
        }

        it 'PSCustomObject' {
            $ItemNode = $Node.GetChildNode('PSCustomObject')
            $ItemNode -is [PSNode]           | Should -BeTrue
            $ItemNode -is [PSCollectionNode] | Should -BeTrue
            $ItemNode -is [PSMapNode]        | Should -BeTrue
            $ItemNode -is [PSObjectNode]     | Should -BeTrue
            $ItemNode.ValueType | Should -Be $Object.PSCustomObject.GetType()
        }
    }

    Context 'Get descendant node' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'String' {
            $ItemNode = $Node.GetNode('String')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.String
            $ItemNode.ValueType | Should -Be $Object.String.GetType()
        }

        it 'String' {
            $ItemNode = $Node.GetNode('.String')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.String
            $ItemNode.ValueType | Should -Be $Object.String.GetType()
        }

        it 'Array' {
            $ItemNode = $Node.GetNode('.Array[0].Comment')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.Array[0].Comment
            $ItemNode.ValueType | Should -Be $Object.Array[0].Comment.GetType()
        }
    }

    Context 'Get and set item' {
        BeforeAll {
            $Test = @{
                String = 'Hello World'
                Array = @(
                    @{ Index = 1; Name = 'One';   Comment = 'First item'  }
                    @{ Index = 2; Name = 'Two';   Comment = 'Second item' }
                    @{ Index = 3; Name = 'Three'; Comment = 'Third item'  }
                )
                HashTable = @{ One = 1; Two = 2; Three = 3 }
                PSCustomObject = [PSCustomObject]@{ One = 1; Two = 2; Three = 3 }
            }

            $Node = [PSNode]::ParseInput($Test)
        }

        it 'Read string node' {
            $ItemNode = $Node.GetChildNode('String')
            $ItemNode.Value | Should -Be 'Hello World'
        }

        it 'Write string node' {
            $ItemNode = $Node.GetChildNode('String')
            $ItemNode.Value = 'Changed'
            $ItemNode.Value                    | Should -Be 'Changed'
            $Node.GetChildNode('String').Value | Should -Be 'Changed'
        }

        it 'Array' {
            $ItemNode = $Node.GetChildNode('Array')
            $ItemNode.GetChildNode(1).GetChildNode('Comment').Value | Should -Be 'Second item'
            $ItemNode.SetValue(1, @{ Index = 2; Name = 'Two'; Comment = 'Foo Bar' })
            $ItemNode.GetChildNode(1).GetChildNode('Comment').Value | Should -Be  'Foo Bar'
        }

        it 'HashTable' {
            $ItemNode = $Node.GetChildNode('HashTable')
            $ItemNode.GetChildNode('Two').Value | Should -Be 2
            $ItemNode.SetValue('Two', 22)
            $ItemNode.GetChildNode('Two').Value | Should -Be 22
        }

        it 'PSCustomObject' {
            $ItemNode = $Node.GetChildNode('PSCustomObject')
            $ItemNode.GetChildNode('Two').Value | Should -Be 2
            $ItemNode.SetValue('Two', 22)
            $ItemNode.GetChildNode('Two').Value | Should -Be 22
        }
    }

    Context 'Get child nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'Array' {
            $ItemNodes = $Node.GetChildNode('Array').ChildNodes
            $ItemNodes -is [Object[]] | Should -BeTrue
            $ItemNodes.Count          | Should -Be 3
            $ItemNodes.Value.Index    | Should -Contain 2
            $ItemNodes.Value.Name     | Should -Be 'One', 'Two', 'Three'
        }

        it 'HashTable' {
            $ItemNodes = $Node.GetChildNode('HashTable').ChildNodes
            $ItemNodes -is [Object[]] | Should -BeTrue
            $ItemNodes.Count          | Should -Be 3
            $ItemNodes.Name           | Should -Contain 'One'
            $ItemNodes.Name           | Should -Contain 'Two'
            $ItemNodes.Name           | Should -Contain 'Three'
            $ItemNodes.Value          | Should -Contain 1
            $ItemNodes.Value          | Should -Contain 2
            $ItemNodes.Value          | Should -Contain 3
        }

        it 'PSCustomObject' {
            $ItemNodes = $Node.GetChildNode('PSCustomObject').ChildNodes
            $ItemNodes -is [Object[]] | Should -BeTrue
            $ItemNodes.Count          | Should -Be 3
            $ItemNodes.Name           | Should -Be 'One', 'Two', 'Three'
            $ItemNodes.Value          | Should -Be 1, 2, 3
        }
    }

    Context 'Get descendant nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'All' {
            $ItemNodes = $Node.DescendantNodes
            $ItemNodes.Count | Should -Be 22
        }
    }

    Context 'Get leaf nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'All' {
            $ItemNodes = $Node.LeafNodes
            $ItemNodes.Count | Should -Be 16
        }
    }

    Context 'Path' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'Comment path' {
            $Node.GetChildNode('Array').ChildNodes[1].GetChildNode('Comment').Path | Should -Be 'Array[1].Comment'
        }

        it 'Using variable name' {
            $Node.GetChildNode('Array').ChildNodes[1].GetChildNode('Comment').GetPathName('$MyObject') | Should -Be '$MyObject.Array[1].Comment'
        }

        it 'Array' {
            $ItemNode = $Node.GetChildNode('Array').GetChildNode(2)
            $ItemNode.Path           | Should -Be 'Array[2]'
            $ItemNode.Path.Nodes[0]  | Should -Be $ItemNode.RootNode
            $ItemNode.Path.Nodes[-1] | Should -Be $ItemNode
            $ItemNode.Path.Nodes[-2] | Should -Be $ItemNode.ParentNode
        }

        it 'HashTable' {
            $ItemNode = $Node.GetChildNode('HashTable').GetChildNode('Two')
            $ItemNode.Path           | Should -Be 'HashTable.Two'
            $ItemNode.Path.Nodes[0]  | Should -Be $ItemNode.RootNode
            $ItemNode.Path.Nodes[-1] | Should -Be $ItemNode
            $ItemNode.Path.Nodes[-2] | Should -Be $ItemNode.ParentNode
        }

        it 'PSCustomObject' {
            $ItemNode = $Node.GetChildNode('PSCustomObject').GetChildNode('Two')
            $ItemNode.Path           | Should -Be 'PSCustomObject.Two'
            $ItemNode.Path.Nodes[0]  | Should -Be $ItemNode.RootNode
            $ItemNode.Path.Nodes[-1] | Should -Be $ItemNode
            $ItemNode.Path.Nodes[-2] | Should -Be $ItemNode.ParentNode
        }

        it 'Path with space' {
            [PSNode]::ParseInput(@{ 'Hello World' = 42 }).ChildNodes[0].Path | Should -Be "'Hello World'"
        }
    }

    Context 'Recursively iterate' {

        it 'Get node paths' {
            $Actual = Iterate ([PSNode]::ParseInput($Object))
            $Expected = '', 'PSCustomObject', 'PSCustomObject.One', 'PSCustomObject.Two', 'PSCustomObject.Three', 'Array', 'Array[0]', 'Array[0].Comment', 'Array[0].Name', 'Array[0].Index', 'Array[1]', 'Array[1].Comment', 'Array[1].Name', 'Array[1].Index', 'Array[2]', 'Array[2].Comment', 'Array[2].Name', 'Array[2].Index', 'HashTable', 'HashTable.One', 'HashTable.Three', 'HashTable.Two', 'String'
            $Actual | Compare-Object $Expected | Should -BeNullOrEmpty
        }
    }


    Context '(not) exists' {
        BeforeAll {
            $Exists = @{
                String = 'Hello World'
                Array = 'One', 'Two', 'Three'
                HashTable = @{ One = 1; Two = 2; Three = 3 }
                PSCustomObject = [PSCustomObject]@{ One = 1; Two = 2; Three = 3 }
            }
            $Node = [PSNode]::ParseInput($Exists)
        }

        it '(in/out bound)' {
            { $Node.GetChildNode('NotExists') }                           | Should -Throw
            { $Node.GetChildNode('String') }                              | Should -Not -Throw
            { $Node.GetChildNode('Array') }                               | Should -Not -Throw
            { $Node.GetChildNode('Array').GetChildNode(2)}                | Should -Not -Throw
            { $Node.GetChildNode('Array').GetChildNode(3)}                | Should -Throw
            { $Node.GetChildNode('Array').GetChildNode(-3) }              | Should -Not -Throw
            { $Node.GetChildNode('Array').GetChildNode(-4) }              | Should -Throw
            { $Node.GetChildNode('HashTable').GetChildNode('One') }       | Should -Not -Throw
            { $Node.GetChildNode('HashTable').GetChildNode('one') }       | Should -Not -Throw
            { $Node.GetChildNode('HashTable').GetChildNode('Four') }      | Should -Throw
            { $Node.GetChildNode('PSCustomObject').GetChildNode('One') }  | Should -Not -Throw
            { $Node.GetChildNode('PSCustomObject').GetChildNode('one') }  | Should -Not -Throw
            { $Node.GetChildNode('PSCustomObject').GetChildNode('Four') } | Should -Throw
        }
    }

    Context 'Null or Empty' {
        BeforeAll {
            $Empties = @{
                String = ''
                Array = @()
                HashTable = @{}
                PSCustomObject = [PSCustomObject]@{}
            }
            $Node = [PSNode]::ParseInput($Empties)
        }

        it 'Empty Array' {
            $ArrayNode = $Node.GetChildNode('Array')
            $ItemNodes = $ArrayNode.ChildNodes
            $ItemNodes | Should -BeNullOrEmpty
        }

        it 'Empty HashTable' {
            $HashTableNode = $Node.GetChildNode('HashTable')
            $ItemNodes = $HashTableNode.ChildNodes
            $ItemNodes | Should -BeNullOrEmpty
        }

        it 'Empty PSCustomObject' {
            $PSCustomObjectNode = $Node.GetChildNode('PSCustomObject')
            $ItemNodes = $PSCustomObjectNode.ChildNodes
            $ItemNodes | Should -BeNullOrEmpty
        }
    }

    Context 'Add node' {

        It 'Array' {
            $ArrayNode = ,@('a', 'b', 'c') | Get-Node
            $ArrayNode.Add('d')
            ,$ArrayNode.Value | Should -BeOfType Array
            $ArrayNode.Value  | Should -be 'a', 'b', 'c', 'd'
        }

        It 'List' {
            $ListNode = ,[System.Collections.Generic.List[Object]]@('a', 'b', 'c') | Get-Node
            $ListNode.Add('d')
            ,$ListNode.Value | Should -BeOfType System.Collections.Generic.List[Object]
            $ListNode.Value  | Should -be 'a', 'b', 'c', 'd'
        }

        It 'String[]' {
            $Strings = ,[String[]]@('a', 'b', 'c') | Get-Node
            $Strings.Add('d')
            ,$Strings.Value | Should -BeOfType String[]
            $Strings.Value  | Should -be 'a', 'b', 'c', 'd'
        }

        It 'HashTable' {
            $HashTableNode = @{ a = 1; b = 2; c = 3 } | Get-Node
            $HashTableNode.Add('d', 4)
            $HashTableNode.Value        | Should -BeOfType HashTable
            $HashTableNode.Value.Keys   | Sort-Object  | Should -be 'a', 'b', 'c', 'd'
            $HashTableNode.Value.Values | Sort-Object  | Should -be 1, 2, 3, 4
        }

        It 'Ordered' {
            $OrderedNode = [Ordered]@{ a = 1; b = 2; c = 3 } | Get-Node
            $OrderedNode.Add('d', 4)
            $OrderedNode.Value        | Should -BeOfType System.Collections.Specialized.OrderedDictionary
            $OrderedNode.Value.Keys   | Should -be 'a', 'b', 'c', 'd'
            $OrderedNode.Value.Values | Should -be 1, 2, 3, 4
        }

        It 'Dictionary' {
            $DictionaryNode = [System.Collections.Generic.Dictionary[String,Int]]::new() | Get-Node
            $DictionaryNode.Add('a', 1)
            $DictionaryNode.Add('b', 2)
            $DictionaryNode.Add('c', 3)
            $DictionaryNode.Add('d', 4)
            $DictionaryNode.Value        | Should -BeOfType 'System.Collections.Generic.Dictionary[String,Int]'
            $DictionaryNode.Value.Keys   | Should -be 'a', 'b', 'c', 'd'
            $DictionaryNode.Value.Values | Should -be 1, 2, 3, 4
        }

        It 'PSCustomObject' {
            $PSCustomObject = [PSCustomObject]@{ a = 1; b = 2; c = 3 } | Get-Node
            $PSCustomObject.Add('d', 4)
            $PSCustomObject.Value                           | Should -BeOfType System.Management.Automation.PSCustomObject
            $PSCustomObject.Value.PSObject.Properties.Name  | Should -be 'a', 'b', 'c', 'd'
            $PSCustomObject.Value.PSObject.Properties.Value | Should -be 1, 2, 3, 4
        }
    }

    Context 'Remove node' {

        It 'Array' {
            $ArrayNode = ,@('a', 'b', 'c', 'd') | Get-Node
            $ArrayNode.Remove('b')
            ,$ArrayNode.Value | Should -BeOfType Array
            $ArrayNode.Value  | Should -be 'a', 'c', 'd'
        }

        It 'List' {
            $ListNode = ,[System.Collections.Generic.List[Object]]@('a', 'b', 'c', 'd') | Get-Node
            $ListNode.Remove('b')
            ,$ListNode.Value | Should -BeOfType System.Collections.Generic.List[Object]
            $ListNode.Value  | Should -be 'a', 'c', 'd'
        }

        It 'String[]' {
            $Strings = ,[String[]]@('a', 'b', 'c', 'd') | Get-Node
            $Strings.Remove('b')
            ,$Strings.Value | Should -BeOfType String[]
            $Strings.Value  | Should -be 'a', 'c', 'd'
        }

        It 'HashTable' {
            $HashTableNode = @{ a = 1; b = 2; c = 3; d = 4 } | Get-Node
            $HashTableNode.remove('b')
            $HashTableNode.Value        | Should -BeOfType HashTable
            $HashTableNode.Value.Keys   | Sort-Object  | Should -be 'a', 'c', 'd'
            $HashTableNode.Value.Values | Sort-Object  | Should -be 1, 3, 4
        }

        It 'Ordered' {
            $OrderedNode = [Ordered]@{ a = 1; b = 2; c = 3; d = 4 } | Get-Node
            $OrderedNode.remove('b')
            $OrderedNode.Value        | Should -BeOfType System.Collections.Specialized.OrderedDictionary
            $OrderedNode.Value.Keys   | Should -be 'a', 'c', 'd'
            $OrderedNode.Value.Values | Should -be 1, 3, 4
        }

        It 'Dictionary' {
            $DictionaryNode = [System.Collections.Generic.Dictionary[String,Int]]::new() | Get-Node
            $DictionaryNode.Add('a', 1)
            $DictionaryNode.Add('b', 2)
            $DictionaryNode.Add('c', 3)
            $DictionaryNode.Add('d', 4)
            $DictionaryNode.remove('b')
            $DictionaryNode.Value        | Should -BeOfType 'System.Collections.Generic.Dictionary[String,Int]'
            $DictionaryNode.Value.Keys   | Should -be 'a', 'c', 'd'
            $DictionaryNode.Value.Values | Should -be 1, 3, 4
        }

        It 'PSCustomObject' {
            $PSCustomObject = [PSCustomObject]@{ a = 1; b = 2; c = 3; d = 4 } | Get-Node
            $PSCustomObject.Remove('b')
            $PSCustomObject.Value                           | Should -BeOfType System.Management.Automation.PSCustomObject
            $PSCustomObject.Value.PSObject.Properties.Name  | Should -be 'a', 'c', 'd'
            $PSCustomObject.Value.PSObject.Properties.Value | Should -be 1, 3, 4
        }
    }

    Context 'Remove me' {

        It 'Array' {
            $ArrayNode = ,@('a', 'b', 'c', 'd') | Get-Node
            $bNode = $ArrayNode | Get-Node '[1]'
            $bNode.Remove()
            ,$ArrayNode.Value | Should -BeOfType Array
            $ArrayNode.Value  | Should -be 'a', 'c', 'd'
        }

        It 'List' {
            $ListNode = ,[System.Collections.Generic.List[Object]]@('a', 'b', 'c', 'd') | Get-Node
            $bNode = $ListNode | Get-Node '[1]'
            $bNode.Remove()
            ,$ListNode.Value | Should -BeOfType System.Collections.Generic.List[Object]
            $ListNode.Value  | Should -be 'a', 'c', 'd'
        }

        It 'String[]' {
            $Strings = ,[String[]]@('a', 'b', 'c', 'd') | Get-Node
            $bNode = $Strings | Get-Node '[1]'
            $bNode.Remove()
            ,$Strings.Value | Should -BeOfType String[]
            $Strings.Value  | Should -be 'a', 'c', 'd'
        }

        It 'HashTable' {
            $HashTableNode = @{ a = 1; b = 2; c = 3; d = 4 } | Get-Node
            $bNode = $HashTableNode | Get-Node b
            $bNode.Remove()
            $HashTableNode.Value        | Should -BeOfType HashTable
            $HashTableNode.Value.Keys   | Sort-Object  | Should -be 'a', 'c', 'd'
            $HashTableNode.Value.Values | Sort-Object  | Should -be 1, 3, 4
        }

        It 'Ordered' {
            $OrderedNode = [Ordered]@{ a = 1; b = 2; c = 3; d = 4 } | Get-Node
            $bNode = $OrderedNode | Get-Node b
            $bNode.Remove()
            $OrderedNode.Value        | Should -BeOfType System.Collections.Specialized.OrderedDictionary
            $OrderedNode.Value.Keys   | Should -be 'a', 'c', 'd'
            $OrderedNode.Value.Values | Should -be 1, 3, 4
        }

        It 'Dictionary' {
            $DictionaryNode = [System.Collections.Generic.Dictionary[String,Int]]::new() | Get-Node
            $DictionaryNode.Add('a', 1)
            $DictionaryNode.Add('b', 2)
            $DictionaryNode.Add('c', 3)
            $DictionaryNode.Add('d', 4)
            $bNode = $DictionaryNode | Get-Node b
            $bNode.Remove()
            $DictionaryNode.Value        | Should -BeOfType 'System.Collections.Generic.Dictionary[String,Int]'
            $DictionaryNode.Value.Keys   | Should -be 'a', 'c', 'd'
            $DictionaryNode.Value.Values | Should -be 1, 3, 4
        }

        It 'PSCustomObject' {
            $PSCustomObject = [PSCustomObject]@{ a = 1; b = 2; c = 3; d = 4 } | Get-Node
            $bNode = $PSCustomObject | Get-Node b
            $bNode.Remove()
            $PSCustomObject.Value                           | Should -BeOfType System.Management.Automation.PSCustomObject
            $PSCustomObject.Value.PSObject.Properties.Name  | Should -be 'a', 'c', 'd'
            $PSCustomObject.Value.PSObject.Properties.Value | Should -be 1, 3, 4
        }
    }

    Context 'CaseMatters' {

        It 'Case insensitive dictionary' {
            (@{} | Get-Node).CaseMatters | Should -BeNullOrEmpty
            (@{ 1 = 'a' } | Get-Node).CaseMatters | Should -BeNullOrEmpty
            (@{ abc = 1 } | Get-Node).CaseMatters | Should -BeFalse
            (@{ abc = 1; def = 2 } | Get-Node).CaseMatters | Should -BeFalse
        }

        It 'Case insensitive object' {
            ([PSCustomObject]@{} | Get-Node).CaseMatters | Should -BeFalse
            ([PSCustomObject]@{ 1 = 'a' } | Get-Node).CaseMatters | Should -BeFalse
            ([PSCustomObject]@{ abc = 1 } | Get-Node).CaseMatters | Should -BeFalse
            ([PSCustomObject]@{ abc = 1; def = 2 } | Get-Node).CaseMatters | Should -BeFalse
        }

        It 'Case sensitive dictionary' {
            $HashTable = [HashTable]::new()
            ($HashTable | Get-Node).CaseMatters | Should -BeNullOrEmpty
            $HashTable.Add(1, 'a')
            ($HashTable | Get-Node).CaseMatters | Should -BeNullOrEmpty
            $HashTable.Add('abc', 1)
            ($HashTable | Get-Node).CaseMatters | Should -BeTrue
            $HashTable.Add('def', 2)
            ($HashTable | Get-Node).CaseMatters | Should -BeTrue
            $HashTable.Add('ABC', 3)
            ($HashTable | Get-Node).CaseMatters | Should -BeTrue
        }
    }
}