Tests/VSAModule.NestedObject.Tests.ps1

BeforeAll {
    $script:ModuleRoot = Split-Path -Path (Split-Path -Parent $PSCommandPath)
    Import-Module (Join-Path $script:ModuleRoot 'VSAModule.psd1') -Force
}

Describe "ConvertTo-VSAHashtable normalizes every nested-object input form" {

    It "returns an empty hashtable for `$null" {
        InModuleScope VSAModule {
            $ht = ConvertTo-VSAHashtable $null
            $ht | Should -BeOfType ([hashtable])
            $ht.Count | Should -Be 0
        }
    }

    It "passes a hashtable through, copying (not aliasing) it" {
        InModuleScope VSAModule {
            $src = @{ City = 'New York'; Zip = '10001' }
            $ht  = ConvertTo-VSAHashtable $src
            $ht.City | Should -Be 'New York'
            $ht.Zip  | Should -Be '10001'
            # mutating the result must not touch the caller's object
            $ht['City'] = 'Boston'
            $src.City | Should -Be 'New York'
        }
    }

    It "converts a PSCustomObject's NoteProperties to keys" {
        InModuleScope VSAModule {
            $obj = [pscustomobject]@{ PrimaryEmail = 'a@b.com'; Country = 'US' }
            $ht  = ConvertTo-VSAHashtable $obj
            $ht.PrimaryEmail | Should -Be 'a@b.com'
            $ht.Country      | Should -Be 'US'
        }
    }

    It "parses the legacy brace form '{ k= v; k2= v2 }'" {
        InModuleScope VSAModule {
            $ht = ConvertTo-VSAHashtable '{ City= New York; Country= US }'
            $ht.City    | Should -Be 'New York'
            $ht.Country | Should -Be 'US'
        }
    }

    It "parses the legacy plain newline form 'k=v`nk2=v2'" {
        InModuleScope VSAModule {
            $ht = ConvertTo-VSAHashtable "City=New York`nCountry=US"
            $ht.City    | Should -Be 'New York'
            $ht.Country | Should -Be 'US'
        }
    }

    It "keeps a value that itself contains '=' (splits on first '=' only)" {
        InModuleScope VSAModule {
            $ht = ConvertTo-VSAHashtable '{ Token= a=b=c }'
            $ht.Token | Should -Be 'a=b=c'
        }
    }

    It "preserves a value containing ';' when passed structurally (no corruption)" {
        InModuleScope VSAModule {
            $ht = ConvertTo-VSAHashtable @{ PrimaryPhone = '555;x99' }
            $ht.PrimaryPhone | Should -Be '555;x99'
        }
    }

    It "does not treat a backslash as an escape sequence (unlike ConvertFrom-StringData)" {
        InModuleScope VSAModule {
            $ht = ConvertTo-VSAHashtable @{ Path = 'C:\Temp\New' }
            $ht.Path | Should -Be 'C:\Temp\New'
        }
    }

    It "returns empty for a whitespace-only / empty string" {
        InModuleScope VSAModule {
            (ConvertTo-VSAHashtable '').Count      | Should -Be 0
            (ConvertTo-VSAHashtable ' ').Count   | Should -Be 0
            (ConvertTo-VSAHashtable '{}').Count    | Should -Be 0
        }
    }
}

Describe "New-VSAOrganization nested objects reach the request body" {

    It "accepts -ContactInfo as a hashtable and nests it under ContactInfo" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' `
                -ContactInfo @{ PrimaryEmail = 'a@b.com'; City = 'New York'; PrimaryPhone = '555;x99' } | Out-Null
            $obj = $script:body | ConvertFrom-Json
            $obj.ContactInfo.PrimaryEmail | Should -Be 'a@b.com'
            $obj.ContactInfo.City         | Should -Be 'New York'
            $obj.ContactInfo.PrimaryPhone | Should -Be '555;x99'
            # discrete contact fields must not leak to the top level
            $obj.PSObject.Properties.Name | Should -Not -Contain 'PrimaryEmail'
        }
    }

    It "still accepts the legacy -ContactInfo brace string" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' `
                -ContactInfo '{ PrimaryEmail= a@b.com; City= New York }' | Out-Null
            $obj = $script:body | ConvertFrom-Json
            $obj.ContactInfo.PrimaryEmail | Should -Be 'a@b.com'
            $obj.ContactInfo.City         | Should -Be 'New York'
        }
    }

    It "builds ContactInfo from discrete parameters when -ContactInfo is not supplied" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' -PrimaryEmail 'x@y.com' -City 'Reno' | Out-Null
            $obj = $script:body | ConvertFrom-Json
            $obj.ContactInfo.PrimaryEmail | Should -Be 'x@y.com'
            $obj.ContactInfo.City         | Should -Be 'Reno'
        }
    }

    It "accepts -CustomFields as an array of hashtables, one object per element (no flattening)" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' -CustomFields @(
                @{ FieldName = 'Region'; FieldValue = 'EMEA' },
                @{ FieldName = 'Tier';   FieldValue = 'Gold' }
            ) | Out-Null
            $obj = $script:body | ConvertFrom-Json
            @($obj.CustomFields).Count | Should -Be 2
            ($obj.CustomFields | Where-Object FieldName -eq 'Region').FieldValue | Should -Be 'EMEA'
            ($obj.CustomFields | Where-Object FieldName -eq 'Tier').FieldValue   | Should -Be 'Gold'
        }
    }

    It "serializes a SINGLE -CustomFields element as a JSON array, not a bare object (live-found regression)" {
        # Deserializing back through ConvertFrom-Json hides this bug (a bare object round-trips just
        # like a 1-element array under Where-Object), so this checks the raw JSON text for the '['
        # array marker -- exactly what the live VSA API rejects with an HTTP 400 when missing.
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' -CustomFields @(
                @{ FieldName = 'Region'; FieldValue = 'EMEA' }
            ) | Out-Null
            $script:body | Should -Match '"CustomFields":\s*\['
        }
    }

    It "serializes a single -FieldName/-FieldValue pair as a JSON array too" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' -FieldName 'Region' -FieldValue 'EMEA' | Out-Null
            $script:body | Should -Match '"CustomFields":\s*\['
        }
    }

    It "accepts -Attributes as a hashtable" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAOrganization -OrgName 'Acme' -OrgRef 'acme' -Attributes @{ Segment = 'SMB' } | Out-Null
            $obj = $script:body | ConvertFrom-Json
            $obj.Attributes.Segment | Should -Be 'SMB'
        }
    }
}

Describe "Attributes-only cmdlets accept a native hashtable" {

    It "New-VSAScope nests -Attributes hashtable" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSAScope -ScopeName 'S1' -Attributes @{ Segment = 'SMB' } | Out-Null
            ($script:body | ConvertFrom-Json).Attributes.Segment | Should -Be 'SMB'
        }
    }

    It "New-VSARole nests -Attributes hashtable" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSARole -RoleName 'R1' -RoleTypeIds @('4') -Attributes @{ Segment = 'SMB' } | Out-Null
            ($script:body | ConvertFrom-Json).Attributes.Segment | Should -Be 'SMB'
        }
    }

    It "New-VSADepartment nests -Attributes hashtable" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            New-VSADepartment -OrgId 12345 -DepartmentName 'D1' -Attributes @{ Segment = 'SMB' } | Out-Null
            ($script:body | ConvertFrom-Json).Attributes.Segment | Should -Be 'SMB'
        }
    }

    It "New-VSAMachineGroup nests -Attributes hashtable" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            Mock Get-VSAOrganization { [pscustomobject]@{ OrgRef = 'acme'; OrgId = 12345 } }
            # A non-null connection is required so the internal parent-org lookup binds and the
            # Get-VSAOrganization mock is reached (its -VSAConnection param rejects $null).
            $conn = [VSAConnection]::new()
            New-VSAMachineGroup -VSAConnection $conn -OrgId 12345 -MachineGroupName 'G1' -Attributes @{ Segment = 'SMB' } | Out-Null
            ($script:body | ConvertFrom-Json).Attributes.Segment | Should -Be 'SMB'
        }
    }

    It "New-VSATenant now accepts a real -Attributes hashtable (was declared [hashtable] but string-parsed)" {
        InModuleScope VSAModule {
            $script:body = $null
            Mock Invoke-VSARestMethod { $script:body = $Body }
            $sec = ConvertTo-SecureString 'P@ssw0rd!' -AsPlainText -Force
            New-VSATenant -Ref 'T1' -AdminUserName 'admin' -Password $sec -EMail 'a@b.com' `
                -Attributes @{ Segment = 'SMB' } | Out-Null
            ($script:body | ConvertFrom-Json).Attributes.Segment | Should -Be 'SMB'
        }
    }
}