Tests/VSAModule.AgentProcedureList.Tests.ps1

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

Describe "ConvertFrom-VSAScExport parses the VSA 9 agent-procedure XML (F-72)" {

    BeforeAll {
        # A minimal but faithful ScExport document (namespace + Records + Procedure attributes,
        # including the folderId the field survey confirmed, and a <Body> that must be ignored).
        $script:Xml = @'
<?xml version="1.0" encoding="utf-8"?>
<ScExport xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.kaseya.com/vsa/2008/12/Scripting">
  <Records name="TotalRecords" totalRecords="2" startingRecordIndex="0" currentNumRecords="2" />
  <Procedure name="Proc A" treePres="3" id="659091963" folderId="546051318732114" treeFullPath="Core\Windows\A" shared="true">
    <Body description="ignore me"><Statement name="GetVariable" /></Body>
  </Procedure>
  <Procedure name="Proc B" treePres="2" id="12" folderId="34" treeFullPath="Core\B" shared="false" />
</ScExport>
'@

    }

    It "reads the paging totals from <Records>" {
        InModuleScope VSAModule -Parameters @{ xml = $script:Xml } {
            param($xml)
            $r = ConvertFrom-VSAScExport -Body $xml
            $r.TotalRecords | Should -Be 2
            $r.StartingRecordIndex | Should -Be 0
            $r.CurrentNumRecords | Should -Be 2
        }
    }

    It "projects one object per <Procedure> with the summary fields, not the body" {
        InModuleScope VSAModule -Parameters @{ xml = $script:Xml } {
            param($xml)
            $r = ConvertFrom-VSAScExport -Body $xml
            @($r.Procedures).Count | Should -Be 2
            $a = $r.Procedures[0]
            $a.Id       | Should -Be '659091963'
            $a.Name     | Should -Be 'Proc A'
            $a.Path     | Should -Be 'Core\Windows\A'
            $a.FolderId | Should -Be '546051318732114'
            $a.TreePres | Should -Be '3'
            # No Body leaks into the projection.
            $a.PSObject.Properties.Name | Should -Not -Contain 'Body'
        }
    }

    It "keeps ids as strings (folderId overflows Int32) and Shared as a real boolean" {
        InModuleScope VSAModule -Parameters @{ xml = $script:Xml } {
            param($xml)
            $r = ConvertFrom-VSAScExport -Body $xml
            $r.Procedures[0].Id       | Should -BeOfType ([string])
            $r.Procedures[0].FolderId | Should -BeOfType ([string])
            $r.Procedures[0].Shared   | Should -BeOfType ([bool])
            $r.Procedures[0].Shared   | Should -BeTrue
            $r.Procedures[1].Shared   | Should -BeFalse
        }
    }
}

Describe "Get-VSAAPList fetches the whole XML procedure tree by paging" {

    It "pages via $skip/$top until TotalRecords are retrieved, and accumulates every procedure" {
        InModuleScope VSAModule {
            Mock Update-VSAConnection {}
            # Two pages of a 3-record tree: skip 0 -> two procedures, skip 2 -> the last one.
            Mock Invoke-VSAHttp {
                $skip = 0
                if ($Uri -match '(?:\$|%24)skip=(\d+)') { $skip = [int]$Matches[1] }
                $body = if ($skip -eq 0) {
@'
<ScExport xmlns="http://www.kaseya.com/vsa/2008/12/Scripting">
  <Records totalRecords="3" startingRecordIndex="0" currentNumRecords="2" />
  <Procedure id="1" name="A" treeFullPath="p\A" folderId="10" shared="true" treePres="3" />
  <Procedure id="2" name="B" treeFullPath="p\B" folderId="11" shared="false" treePres="3" />
</ScExport>
'@

                } else {
@'
<ScExport xmlns="http://www.kaseya.com/vsa/2008/12/Scripting">
  <Records totalRecords="3" startingRecordIndex="2" currentNumRecords="1" />
  <Procedure id="3" name="C" treeFullPath="p\C" folderId="12" shared="true" treePres="3" />
</ScExport>
'@

                }
                [pscustomobject]@{ StatusCode = 200; Body = $body }
            }

            $conn = [VSAConnection]::new('https://h', 'u', 'tok', 'pat', [datetime]::Now.AddHours(1), $false, $false)
            $procs = @(Get-VSAAPList -VSAConnection $conn -RecordsPerPage 2)

            $procs.Count | Should -Be 3
            ($procs.Id | Sort-Object) -join ',' | Should -Be '1,2,3'
            Should -Invoke Invoke-VSAHttp -Times 2 -Exactly
        }
    }

    It "makes exactly one request for a tree that fits in a single page" {
        InModuleScope VSAModule {
            Mock Update-VSAConnection {}
            Mock Invoke-VSAHttp {
                [pscustomobject]@{ StatusCode = 200; Body = @'
<ScExport xmlns="http://www.kaseya.com/vsa/2008/12/Scripting">
  <Records totalRecords="1" startingRecordIndex="0" currentNumRecords="1" />
  <Procedure id="9" name="Solo" treeFullPath="p\Solo" folderId="1" shared="false" treePres="1" />
</ScExport>
'@
 }
            }
            $conn = [VSAConnection]::new('https://h', 'u', 'tok', 'pat', [datetime]::Now.AddHours(1), $false, $false)
            (@(Get-VSAAPList -VSAConnection $conn)).Count | Should -Be 1
            Should -Invoke Invoke-VSAHttp -Times 1 -Exactly
        }
    }
}

Describe "Get-VSAAPList is a dedicated XML function, not a JSON dispatcher alias (F-72)" {

    It "is exported as a Function" {
        (Get-Command Get-VSAAPList).CommandType | Should -Be 'Function'
    }

    It "is in FunctionsToExport, not AliasesToExport" {
        $m = Import-PowerShellDataFile (Join-Path $script:ModuleRoot 'VSAModule.psd1')
        $m.FunctionsToExport | Should -Contain 'Get-VSAAPList'
        $m.AliasesToExport   | Should -Not -Contain 'Get-VSAAPList'
    }

    It "is NOT in the JSON dispatcher endpoint map" {
        InModuleScope VSAModule {
            $URISuffixGetMap.ContainsKey('Get-VSAAPList') | Should -BeFalse -Because 'the JSON read path cannot parse its XML body'
        }
    }
}