tests/CollectorTooling.Tests.ps1

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

<#
    Focused regression tests for the collector audit and converter boundary (AB#5923/5924).

    The classifier must distinguish command names structurally:
      * local Get-AZSC helpers are data shaping, not Get-Az network calls;
      * Search-AzGraph is live access despite not starting with Get/Invoke;
      * New-Object is ordinary shaping unless its AST contains -Com/-ComObject.

    The converter must enforce the same boundary before it lifts any source into a definition.
#>


BeforeAll {
    $script:RepoRoot = Split-Path -Parent $PSScriptRoot
    $script:AuditScript = Join-Path $script:RepoRoot 'scripts' 'Invoke-CollectorAudit.ps1'
    $script:ConverterScript = Join-Path $script:RepoRoot 'scripts' 'ConvertTo-ScoutCollectorDefinition.ps1'
    $script:Sandbox = Join-Path ([System.IO.Path]::GetTempPath()) ("azsc-collector-tooling-" + [guid]::NewGuid().ToString('N'))
    $script:InventoryRoot = Join-Path $script:Sandbox 'Modules' 'Public' 'InventoryModules'
    $script:CategoryRoot = Join-Path $script:InventoryRoot 'Test'
    $script:AuditJson = Join-Path $script:Sandbox 'collector-audit.json'
    New-Item -ItemType Directory -Path $script:CategoryRoot -Force | Out-Null

    function New-ToolingCollector {
        param(
            [Parameter(Mandatory)] [string] $Name,
            [Parameter(Mandatory)] [string[]] $ProcessingStatements
        )

        $Path = Join-Path $script:CategoryRoot "$Name.ps1"
        $Lines = @(
            'param($SCPath, $Sub, $Intag, $Resources, $Retirements, $Task, $File, $SmaResources, $TableStyle, $Unsupported)'
            "if (`$Task -eq 'Processing') {"
            " `$Rows = `$Resources | Where-Object { `$_.TYPE -eq 'microsoft.test/widgets' }"
        )
        $Lines += @($ProcessingStatements | ForEach-Object { " $_" })
        $Lines += @(
            ' $tmp = foreach ($1 in $Rows) {'
            " `$obj = @{ 'ID' = `$1.id; 'Name' = `$1.name }"
            ' $obj'
            ' }'
            ' return $tmp'
            '} else {'
            ' $Exc = [System.Collections.Generic.List[string]]::new()'
            " [void]`$Exc.Add('Name')"
            " `$SmaResources | Select-Object -Property `$Exc | Export-Excel -Path `$File -WorksheetName '$Name' -TableName '${Name}Table_1'"
            '}'
        )
        Set-Content -LiteralPath $Path -Value $Lines -Encoding utf8
        return $Path
    }

    $script:SafePath = New-ToolingCollector -Name 'SafeHelpers' -ProcessingStatements @(
        "`$null = Get-AZSCSafeProperty -InputObject `$Rows -Path 'name' -Enumerate"
        "`$null = Get-AZSCIdSegment -Id '/subscriptions/sub/resourceGroups/rg/providers/microsoft.test/widgets/one' -Index 8"
        "`$null = Get-AZSCCollectedValue -InputObject `$Rows -Name 'name'"
        '$null = New-Object System.Collections.Generic.List[string]'
    )
    $script:GraphPath = New-ToolingCollector -Name 'GraphAccess' -ProcessingStatements @(
        "`$null = Search-AzGraph -Query 'resources | take 1'"
    )
    $script:ComPath = New-ToolingCollector -Name 'ComAccess' -ProcessingStatements @(
        "`$null = New-Object -Com 'HTMLFile'"
    )

    $null = & $script:AuditScript -InventoryRoot $script:InventoryRoot -OutputJson $script:AuditJson
    $script:Audit = @(Get-Content -LiteralPath $script:AuditJson -Raw | ConvertFrom-Json)
}

AfterAll {
    if (Test-Path -LiteralPath $script:Sandbox) {
        Remove-Item -LiteralPath $script:Sandbox -Recurse -Force
    }
}

Describe 'Collector audit external-access classification' {
    It 'does not classify the three in-process Get-AZSC helpers or ordinary New-Object as live access' {
        $Record = $script:Audit | Where-Object Name -eq 'SafeHelpers'
        $Record.Classification | Should -Be 'PureShaping'
        @($Record.ExternalCalls).Count | Should -Be 0
    }

    It 'detects Search-AzGraph as live access' {
        $Record = $script:Audit | Where-Object Name -eq 'GraphAccess'
        $Record.Classification | Should -Be 'EscapeHatch'
        @($Record.ExternalCalls) | Should -Contain 'Search-AzGraph'
    }

    It 'detects New-Object -Com as live access without banning ordinary New-Object' {
        $Record = $script:Audit | Where-Object Name -eq 'ComAccess'
        $Record.Classification | Should -Be 'EscapeHatch'
        @($Record.ExternalCalls) | Should -Contain 'New-Object -Com'
    }
}

Describe 'Collector converter external-access guard' {
    It 'still converts a collector that uses only the three in-process helpers' {
        $Definition = & $script:ConverterScript -CollectorPath $script:SafePath -Show
        ($Definition -join "`n") | Should -Match "SourceCollector"
    }

    It '<Name> refuses conversion before emitting a definition' -ForEach @(
        @{ Name = 'Search-AzGraph'; PathVariable = 'GraphPath'; Expected = 'Search-AzGraph' }
        @{ Name = 'New-Object -Com'; PathVariable = 'ComPath'; Expected = 'New-Object -Com' }
    ) {
        $CollectorPath = Get-Variable -Name $PathVariable -Scope Script -ValueOnly
        { & $script:ConverterScript -CollectorPath $CollectorPath -Show } |
            Should -Throw -ExpectedMessage "*live external access*$Expected*"
    }
}