tests/GetAZSCSafeProperty.Tests.ps1
|
#Requires -Version 7.0 #Requires -Modules Pester <# AB#5667 -- Get-AZSCSafeProperty is the single-object counterpart to Get-AZSCCollectedValue (see StrictModeMemberEnumeration.Tests.ps1): where that function protects a COLLECTION read from throwing when member enumeration yields nothing, this one protects a single object's property CHAIN from throwing when an intermediate segment is genuinely absent (not $null -- absent). Real Azure API responses omit optional blocks (licenseType, availabilitySet, diagnosticsProfile...) entirely on resources that do not use them. #> BeforeAll { . (Join-Path (Split-Path -Parent $PSScriptRoot) 'Modules/Private/Main/Get-AZSCSafeProperty.ps1') } Describe 'Get-AZSCSafeProperty' { It 'returns the value at the end of a path that is fully present' { Set-StrictMode -Version Latest $obj = [pscustomobject]@{ a = [pscustomobject]@{ b = [pscustomobject]@{ c = 'value' } } } Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c' | Should -Be 'value' } It 'returns $null, not a throw, when the final segment is absent' { Set-StrictMode -Version Latest $obj = [pscustomobject]@{ a = [pscustomobject]@{ b = [pscustomobject]@{} } } { Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c' } | Should -Not -Throw Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c' | Should -BeNullOrEmpty } It 'returns $null, not a throw, when an INTERMEDIATE segment is absent' { Set-StrictMode -Version Latest $obj = [pscustomobject]@{ a = [pscustomobject]@{} } { Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c.d' } | Should -Not -Throw Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c.d' | Should -BeNullOrEmpty } It 'returns $null, not a throw, when an intermediate value is explicitly $null' { Set-StrictMode -Version Latest $obj = [pscustomobject]@{ a = $null } { Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c' } | Should -Not -Throw Get-AZSCSafeProperty -InputObject $obj -Path 'a.b.c' | Should -BeNullOrEmpty } It 'returns $null, not a throw, when the input object itself is $null' { Set-StrictMode -Version Latest { Get-AZSCSafeProperty -InputObject $null -Path 'a.b.c' } | Should -Not -Throw Get-AZSCSafeProperty -InputObject $null -Path 'a.b.c' | Should -BeNullOrEmpty } It 'reads a single-segment path' { Set-StrictMode -Version Latest Get-AZSCSafeProperty -InputObject ([pscustomobject]@{ x = 42 }) -Path 'x' | Should -Be 42 } It 'reads through a hashtable / IDictionary element' { Set-StrictMode -Version Latest $obj = @{ a = @{ b = 'from-hashtable' } } Get-AZSCSafeProperty -InputObject $obj -Path 'a.b' | Should -Be 'from-hashtable' } It 'preserves an explicit $false rather than treating it as absent' { Set-StrictMode -Version Latest $obj = [pscustomobject]@{ a = $false } Get-AZSCSafeProperty -InputObject $obj -Path 'a' | Should -BeExactly $false } It 'accepts the object from the pipeline' { Set-StrictMode -Version Latest ([pscustomobject]@{ a = 'piped' } | Get-AZSCSafeProperty -Path 'a') | Should -Be 'piped' } } Describe 'Get-AZSCSafeProperty -Enumerate reproduces member enumeration (AB#5671)' { # WHY THIS SWITCH EXISTS, and why every test below compares against the RAW chain rather than # against a hand-written expectation. # # Plenty of collector chains pass THROUGH an array: # $data.virtualMachineProfile.networkProfile.networkInterfaceConfigurations.properties.nsg.id # Unguarded PowerShell enumerates that array, reads the segment off every element and flattens # the results. The plain single-object walk does not -- an array has no NoteProperty called # `properties`, so it returns $null. Converting such a chain WITHOUT -Enumerate would therefore # not be a StrictMode fix at all: it would silently start emitting $null where a populated # resource previously emitted a real value. That is a data regression, and a quiet one. # # So the assertion in each case is "identical to what the raw chain produces with StrictMode # off", because that is precisely the contract: same value, minus the throw. BeforeAll { $script:RepoRoot = Split-Path -Parent $PSScriptRoot . (Join-Path $script:RepoRoot 'Modules' 'Private' 'Main' 'Get-AZSCSafeProperty.ps1') function Assert-SameAsRawChain { param( [Parameter(Mandatory)] $InputObject, [Parameter(Mandatory)] [string] $Path, [Parameter(Mandatory)] [scriptblock] $RawChain ) Set-StrictMode -Off $Expected = & $RawChain Set-StrictMode -Version Latest $Actual = Get-AZSCSafeProperty -InputObject $InputObject -Path $Path -Enumerate if ($null -eq $Expected) { $Actual | Should -BeNullOrEmpty } elseif ($Expected -is [array]) { @($Actual) | Should -Be @($Expected) } else { $Actual | Should -Be $Expected } } } It 'matches the raw chain for a single-element array in the middle of the path' { $o = [pscustomobject]@{ np = [pscustomobject]@{ nics = @( [pscustomobject]@{ properties = [pscustomobject]@{ nsg = [pscustomobject]@{ id = 'nsg-1' } } } ) } } # Note the SCALAR result: PowerShell unwraps a one-element enumeration, and so must this -- # otherwise a caller's `.split('/')` starts seeing an array. Assert-SameAsRawChain -InputObject $o -Path 'np.nics.properties.nsg.id' -RawChain { $o.np.nics.properties.nsg.id } } It 'matches the raw chain for a multi-element array (both ids, in order)' { $o = [pscustomobject]@{ np = [pscustomobject]@{ nics = @( [pscustomobject]@{ properties = [pscustomobject]@{ nsg = [pscustomobject]@{ id = 'nsg-1' } } }, [pscustomobject]@{ properties = [pscustomobject]@{ nsg = [pscustomobject]@{ id = 'nsg-2' } } } ) } } Assert-SameAsRawChain -InputObject $o -Path 'np.nics.properties.nsg.id' -RawChain { $o.np.nics.properties.nsg.id } } It 'returns $null instead of throwing when the intermediate array is EMPTY' { # THE defect class (AB#5633): an empty collection is not $null. The enumeration yields # nothing, and StrictMode reports that as the property being missing. $o = [pscustomobject]@{ np = [pscustomobject]@{ nics = @() } } Set-StrictMode -Version Latest { Get-AZSCSafeProperty -InputObject $o -Path 'np.nics.properties.nsg.id' -Enumerate } | Should -Not -Throw Get-AZSCSafeProperty -InputObject $o -Path 'np.nics.properties.nsg.id' -Enumerate | Should -BeNullOrEmpty } It 'skips elements missing the segment and still returns the ones that have it' { $o = [pscustomobject]@{ np = [pscustomobject]@{ nics = @( [pscustomobject]@{ properties = [pscustomobject]@{} }, [pscustomobject]@{ properties = [pscustomobject]@{ nsg = [pscustomobject]@{ id = 'nsg-2' } } } ) } } Assert-SameAsRawChain -InputObject $o -Path 'np.nics.properties.nsg.id' -RawChain { $o.np.nics.properties.nsg.id } } It 'flattens an array-valued segment, as member enumeration does' { $o = [pscustomobject]@{ a = @( [pscustomobject]@{ b = @('x', 'y') }, [pscustomobject]@{ b = @('z') } ) } Assert-SameAsRawChain -InputObject $o -Path 'a.b' -RawChain { $o.a.b } } It 'enumerates hashtable elements too' { $o = [pscustomobject]@{ a = @( @{ b = 'h1' }, @{ b = 'h2' } ) } Assert-SameAsRawChain -InputObject $o -Path 'a.b' -RawChain { $o.a.b } } It 'does not enumerate a string into its characters' { # A string is IEnumerable. Enumerating it would turn .Length into per-character nonsense. $o = [pscustomobject]@{ a = 'hello' } Assert-SameAsRawChain -InputObject $o -Path 'a.Length' -RawChain { $o.a.Length } } It "prefers the collection's OWN member over enumerating it, as PowerShell does" { # `$array.Count` is the length of the array, NOT a per-element read. Getting this precedence # wrong would silently change the meaning of any path ending in .Count or .Length. $o = [pscustomobject]@{ a = @( [pscustomobject]@{ Count = 'per-element' }, [pscustomobject]@{ Count = 'per-element' } ) } Assert-SameAsRawChain -InputObject $o -Path 'a.Count' -RawChain { $o.a.Count } Get-AZSCSafeProperty -InputObject $o -Path 'a.Count' -Enumerate | Should -Be 2 } It 'leaves the DEFAULT behaviour unchanged -- without the switch, an array stops the walk' { # The switch is opt-in precisely so that the single-object callers that shipped in v2.7.0 # (VirtualMachine.ps1) keep behaving exactly as they were reviewed to behave. $o = [pscustomobject]@{ np = [pscustomobject]@{ nics = @( [pscustomobject]@{ properties = [pscustomobject]@{ nsg = [pscustomobject]@{ id = 'nsg-1' } } } ) } } Set-StrictMode -Version Latest Get-AZSCSafeProperty -InputObject $o -Path 'np.nics.properties.nsg.id' | Should -BeNullOrEmpty } It 'is identical to the plain walk for a pure single-object chain' { $o = [pscustomobject]@{ a = [pscustomobject]@{ b = [pscustomobject]@{ c = 'deep' } } } Set-StrictMode -Version Latest $Plain = Get-AZSCSafeProperty -InputObject $o -Path 'a.b.c' $Enumerated = Get-AZSCSafeProperty -InputObject $o -Path 'a.b.c' -Enumerate $Enumerated | Should -Be $Plain $Enumerated | Should -Be 'deep' } } |