tests/GetAZSCIdSegment.Tests.ps1

#Requires -Version 7.0
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }

<#
    AB#5671 -- Get-AZSCIdSegment, the third of the three StrictMode defect classes in the collectors.
 
    Roughly thirty collectors pull a name out of a related resource's id with a FIXED index, because
    the canonical id
 
        /subscriptions/{s}/resourceGroups/{rg}/providers/{Namespace}/{type}/{name}
         0 1 2 3 4 5 6 7 8
 
    puts the name at 8. Real ids are often shorter -- a subscription-scoped Defender assessment, a
    `managedBy` that is a bare resource group. Without StrictMode an out-of-range index quietly
    returns $null, which is what those collectors were written and tested against; WITH StrictMode it
    throws "Index was outside the bounds of the array", and the collector dies mid-tenant.
 
    The contract asserted here is therefore the same as the other two helpers': the value the
    unguarded expression produced, minus the throw. Nothing changes for an id that IS long enough.
#>


Describe 'Get-AZSCIdSegment (AB#5671)' {

    BeforeAll {
        $script:RepoRoot = Split-Path -Parent $PSScriptRoot
        . (Join-Path $script:RepoRoot 'Modules' 'Private' 'Main' 'Get-AZSCIdSegment.ps1')

        $script:CanonicalId = '/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-test/providers/Microsoft.Network/networkSecurityGroups/nsg-test'
    }

    It 'returns the resource name at index 8 of a canonical id' {
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id $script:CanonicalId -Index 8 | Should -Be 'nsg-test'
    }

    It 'agrees with the unguarded expression for an id that IS long enough' {
        # The whole point: for the ids that already worked, nothing whatsoever changes.
        Set-StrictMode -Off
        $Expected = $script:CanonicalId.split('/')[8]
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id $script:CanonicalId -Index 8 | Should -Be $Expected
    }

    It 'returns the resource group at index 4 and the subnet position at index 10' {
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id $script:CanonicalId -Index 4 | Should -Be 'rg-test'
        $SubnetId = '/subscriptions/s/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet-a/subnets/snet-b'
        Get-AZSCIdSegment -Id $SubnetId -Index 10 | Should -Be 'snet-b'
    }

    It 'returns $null instead of throwing for a SUBSCRIPTION-SCOPED id that has too few segments' {
        # The shape that emptied the Security worksheet: a Defender assessment with no resource target.
        $Short = '/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Security/assessments/abc'
        Set-StrictMode -Version Latest
        { Get-AZSCIdSegment -Id $Short -Index 8 } | Should -Not -Throw
        Get-AZSCIdSegment -Id $Short -Index 8 | Should -BeNullOrEmpty
    }

    It 'proves the raw expression it replaces DOES throw on that same id' {
        # Without this, the test above only shows the helper is quiet -- not that it was needed.
        $Short = '/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Security/assessments/abc'
        Set-StrictMode -Version Latest
        { $Short.split('/')[8] } | Should -Throw -ExpectedMessage '*Index was outside the bounds of the array*'
    }

    It 'returns $null for $null, empty and whitespace ids' -ForEach @(
        @{ Id = $null }
        @{ Id = '' }
    ) {
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id $Id -Index 8 | Should -BeNullOrEmpty
    }

    It 'returns $null for a negative index rather than indexing from the end' {
        # PowerShell's $array[-1] means "last". A collector asking for a negative index is a bug, and
        # silently handing back the resource name would hide it.
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id $script:CanonicalId -Index -1 | Should -BeNullOrEmpty
    }

    It 'handles a CIDR mask, which is what VirtualNetwork.ps1 uses it for' {
        # Not every caller is splitting a resource id: '10.0.0.0/24'.split('/')[1] is the mask, and a
        # subnet with no addressPrefix at all is the out-of-range case there.
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id '10.0.0.0/24' -Index 1 | Should -Be '24'
        Get-AZSCIdSegment -Id '10.0.0.0'    -Index 1 | Should -BeNullOrEmpty
    }

    It 'returns $null for the "0" sentinel some collectors substitute for an empty collection' {
        # AzureAI.ps1 falls back to the string '0' when an account has no private endpoints, so the
        # loop still emits a row; '0' splits to ONE element and index 8 is out of range.
        Set-StrictMode -Version Latest
        Get-AZSCIdSegment -Id '0' -Index 8 | Should -BeNullOrEmpty
    }

    It 'accepts the id from the pipeline' {
        Set-StrictMode -Version Latest
        ($script:CanonicalId | Get-AZSCIdSegment -Index 8) | Should -Be 'nsg-test'
    }

    It 'does not throw when handed an array of ids (a member-enumeration result)' {
        # Chains that cross an array can hand this function several ids at once. The old code did
        # something incoherent in that case too; the requirement here is only that it not throw.
        Set-StrictMode -Version Latest
        { Get-AZSCIdSegment -Id @($script:CanonicalId, $script:CanonicalId) -Index 8 } | Should -Not -Throw
    }
}