diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 976a341..a35d8c1 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -285,7 +285,9 @@ jobs: "${PVE_PASSWORD}" \ "${RUNNER_TEMP}") CLOUD_IMAGE_PATH=$(echo "$OUTPUT" | grep "^CLOUD_IMAGE_PATH=" | cut -d= -f2) + OVA_PATH=$(echo "$OUTPUT" | grep "^OVA_PATH=" | cut -d= -f2) echo "cloud_image_path=${CLOUD_IMAGE_PATH}" >> "$GITHUB_OUTPUT" + echo "ova_path=${OVA_PATH}" >> "$GITHUB_OUTPUT" # ── Run tests ───────────────────────────────────────────────────── @@ -304,6 +306,7 @@ jobs: PVETEST_PVE_VERSION: ${{ matrix.pve_version }} PVETEST_PASSWORD: ${{ env.PVE_PASSWORD }} PVETEST_CLOUD_IMAGE_PATH: ${{ steps.test_env.outputs.cloud_image_path }} + PVETEST_OVA_PATH: ${{ steps.test_env.outputs.ova_path }} run: | $env:PVETEST_ISO_PATH = Join-Path $env:RUNNER_TEMP "pvetest.iso" Import-Module Pester -MinimumVersion 5.0 diff --git a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 index 70456b1..0c1beee 100644 --- a/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 +++ b/tests/PSProxmoxVE.Tests/Integration/Integration.Tests.ps1 @@ -16,6 +16,7 @@ PVETEST_PVE_VERSION - (optional) Expected PVE major version (8 or 9) PVETEST_PASSWORD - (optional) Root password for cloud-init Linux VM provisioning PVETEST_CLOUD_IMAGE_PATH - (optional) Local path to a cloud image (.img) for VM provisioning + PVETEST_OVA_PATH - (optional) Local path to an OVA file for OVA import tests WARNING: These tests CREATE and DESTROY real resources (VMs, users, tokens, roles, snapshots, ISO uploads, etc.) on the target node. @@ -68,13 +69,16 @@ BeforeAll { $script:ExpectedPveVersion = [System.Environment]::GetEnvironmentVariable('PVETEST_PVE_VERSION') $script:Password = [System.Environment]::GetEnvironmentVariable('PVETEST_PASSWORD') $script:CloudImagePath = [System.Environment]::GetEnvironmentVariable('PVETEST_CLOUD_IMAGE_PATH') + $script:OvaPath = [System.Environment]::GetEnvironmentVariable('PVETEST_OVA_PATH') $script:LinuxVmId = $null # Track resources created during the run so AfterAll can clean up. - $script:CreatedVmIds = [System.Collections.Generic.List[int]]::new() - $script:CreatedUsers = [System.Collections.Generic.List[string]]::new() - $script:CreatedRoles = [System.Collections.Generic.List[string]]::new() - $script:TestVmId = $null + $script:CreatedVmIds = [System.Collections.Generic.List[int]]::new() + $script:CreatedContainerIds = [System.Collections.Generic.List[int]]::new() + $script:CreatedUsers = [System.Collections.Generic.List[string]]::new() + $script:CreatedRoles = [System.Collections.Generic.List[string]]::new() + $script:TestVmId = $null + $script:TestContainerId = $null # Helper functions for skip logic function script:Skip-IfNoTarget { @@ -111,6 +115,15 @@ BeforeAll { } return $false } + + function script:Skip-IfNoTestContainer { + if (Skip-IfNoTarget) { return $true } + if ($null -eq $script:TestContainerId) { + Set-ItResult -Skipped -Because 'No test container was created' + return $true + } + return $false + } } AfterAll { @@ -129,6 +142,15 @@ AfterAll { catch { <# non-fatal #> } } + foreach ($ctId in $script:CreatedContainerIds) { + try { + Stop-PveContainer -Node $script:Node -VmId $ctId -Confirm:$false -ErrorAction SilentlyContinue | Out-Null + Start-Sleep -Seconds 3 + Remove-PveContainer -Node $script:Node -VmId $ctId -Force -Purge -Confirm:$false -ErrorAction SilentlyContinue + } + catch { <# non-fatal #> } + } + foreach ($vmId in $script:CreatedVmIds) { try { Stop-PveVm -Node $script:Node -VmId $vmId -Confirm:$false -ErrorAction SilentlyContinue | Out-Null @@ -434,6 +456,51 @@ Describe 'Integration Tests' -Tag 'Integration' { } } + # ----------------------------------------------------------------------- + Context 'VM — Suspend / Resume / Resize' { + It 'Should suspend and resume a VM' { + if (Skip-IfNoTestVm) { return } + + # Start the VM + Start-PveVm -Node $script:Node -VmId $script:TestVmId -Wait | Out-Null + + # Suspend + $suspendTask = Suspend-PveVm -Node $script:Node -VmId $script:TestVmId -Wait + $suspendTask | Should -Not -BeNullOrEmpty + + $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:TestVmId } + $vm.Status | Should -Be 'paused' + + # Resume + $resumeTask = Resume-PveVm -Node $script:Node -VmId $script:TestVmId -Wait + $resumeTask | Should -Not -BeNullOrEmpty + + $vm = Get-PveVm -Node $script:Node | Where-Object { $_.VmId -eq $script:TestVmId } + $vm.Status | Should -Be 'running' + + # Clean up — stop + Stop-PveVm -Node $script:Node -VmId $script:TestVmId -Wait -Confirm:$false | Out-Null + } + + It 'Should resize a VM disk (Resize-PveVmDisk)' { + if (Skip-IfNoTestVm) { return } + + # First add a small disk via Set-PveVmConfig + { Set-PveVmConfig -Node $script:Node -VmId $script:TestVmId ` + -AdditionalConfig @{ scsi0 = 'local-lvm:1' } ` + -ErrorAction Stop } | Should -Not -Throw + + # Resize the disk by +1G + $task = Resize-PveVmDisk -Node $script:Node -VmId $script:TestVmId ` + -Disk 'scsi0' -Size '+1G' + $task | Should -Not -BeNullOrEmpty + + # Verify config shows scsi0 exists (larger disk) + $config = Get-PveVmConfig -Node $script:Node -VmId $script:TestVmId + $config | Should -Not -BeNullOrEmpty + } + } + # ----------------------------------------------------------------------- Context 'Snapshots' { It 'Should create, list, and remove a snapshot' { @@ -518,6 +585,48 @@ Describe 'Integration Tests' -Tag 'Integration' { } } + # ----------------------------------------------------------------------- + Context 'Storage — CRUD' { + It 'Should create a directory storage (New-PveStorage)' { + if (Skip-IfNoTarget) { return } + + $result = New-PveStorage -Storage 'pester-store' -Type 'dir' ` + -Path '/tmp/pester-storage' -Content 'iso,vztmpl,backup' ` + -ErrorAction Stop + + $result | Should -Not -BeNullOrEmpty + } + + It 'Should list and find the new storage (Get-PveStorage)' { + if (Skip-IfNoTarget) { return } + + $storages = Get-PveStorage -Node $script:Node + $storages | Where-Object { $_.Storage -eq 'pester-store' } | + Should -Not -BeNullOrEmpty + } + + It 'Should remove the storage (Remove-PveStorage)' { + if (Skip-IfNoTarget) { return } + + { Remove-PveStorage -Storage 'pester-store' -Confirm:$false -ErrorAction Stop } | + Should -Not -Throw + } + } + + # ----------------------------------------------------------------------- + Context 'Storage — Download' { + It 'Should download a file from URL to storage (Invoke-PveStorageDownload)' { + if (Skip-IfNoTarget) { return } + + $templateUrl = 'http://download.proxmox.com/images/system/alpine-3.20-default_20240908_amd64.tar.xz' + $task = Invoke-PveStorageDownload -Node $script:Node -Storage $script:Storage ` + -Url $templateUrl -Filename 'alpine-3.20-default_20240908_amd64.tar.xz' ` + -ContentType 'vztmpl' -Wait -ErrorAction Stop + + $task | Should -Not -BeNullOrEmpty + } + } + # ----------------------------------------------------------------------- Context 'Tasks' { It 'Should get a task by UPID and wait for completion' { @@ -613,6 +722,99 @@ Describe 'Integration Tests' -Tag 'Integration' { } } + # ----------------------------------------------------------------------- + Context 'SDN' { + BeforeAll { + if ($null -eq $script:SkipReason) { + # SDN requires PVE 8+. Check server version and skip if older. + $detail = Test-PveConnection -Detailed + if ($detail.ServerVersion.Major -lt 8) { + $script:SkipSdn = 'SDN requires Proxmox VE 8.0 or later' + } else { + $script:SkipSdn = $null + } + } else { + $script:SkipSdn = $script:SkipReason + } + } + + It 'Should create an SDN zone (New-PveSdnZone)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + { New-PveSdnZone -Zone 'pester-zone' -Type 'simple' -ErrorAction Stop } | + Should -Not -Throw + } + + It 'Should list SDN zones (Get-PveSdnZone)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + $zones = Get-PveSdnZone + $zones | Should -Not -BeNullOrEmpty + $zones | Where-Object { $_.Zone -eq 'pester-zone' } | + Should -Not -BeNullOrEmpty + } + + It 'Should create an SDN VNet (New-PveSdnVnet)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + { New-PveSdnVnet -Vnet 'pestervn' -Zone 'pester-zone' -ErrorAction Stop } | + Should -Not -Throw + } + + It 'Should list SDN VNets (Get-PveSdnVnet)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + $vnets = Get-PveSdnVnet + $vnets | Should -Not -BeNullOrEmpty + $vnets | Where-Object { $_.Vnet -eq 'pestervn' } | + Should -Not -BeNullOrEmpty + } + + It 'Should create an SDN subnet (New-PveSdnSubnet)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + { New-PveSdnSubnet -Vnet 'pestervn' -Subnet '10.99.0.0/24' -ErrorAction Stop } | + Should -Not -Throw + } + + It 'Should list SDN subnets (Get-PveSdnSubnet)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + $subnets = Get-PveSdnSubnet -Vnet 'pestervn' + $subnets | Should -Not -BeNullOrEmpty + } + + It 'Should remove SDN subnet (Remove-PveSdnSubnet)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + { Remove-PveSdnSubnet -Vnet 'pestervn' -Subnet 'pestervn-10.99.0.0-24' ` + -Confirm:$false -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should remove SDN VNet (Remove-PveSdnVnet)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + { Remove-PveSdnVnet -Vnet 'pestervn' -Confirm:$false -ErrorAction Stop } | + Should -Not -Throw + } + + It 'Should remove SDN zone (Remove-PveSdnZone)' { + if (Skip-IfNoTarget) { return } + if ($script:SkipSdn) { Set-ItResult -Skipped -Because $script:SkipSdn; return } + + { Remove-PveSdnZone -Zone 'pester-zone' -Confirm:$false -ErrorAction Stop } | + Should -Not -Throw + } + } + # ----------------------------------------------------------------------- Context 'Templates' { It 'Should list templates' { @@ -632,6 +834,197 @@ Describe 'Integration Tests' -Tag 'Integration' { { Get-PveCloudInitConfig -Node $script:Node -VmId $script:TestVmId -ErrorAction Stop } | Should -Not -Throw } + + It 'Should regenerate cloud-init image (Invoke-PveCloudInitRegenerate)' { + if (Skip-IfNoLinuxVm) { return } + + # The Linux VM has a cloud-init drive from provisioning + { Invoke-PveCloudInitRegenerate -Node $script:Node -VmId $script:LinuxVmId -Wait -ErrorAction Stop } | + Should -Not -Throw + } + } + + # ----------------------------------------------------------------------- + Context 'Containers' { + BeforeAll { + if ($null -eq $script:SkipReason) { + # Download a small Alpine LXC template + $templateUrl = 'http://download.proxmox.com/images/system/alpine-3.20-default_20240908_amd64.tar.xz' + try { + Invoke-PveStorageDownload -Node $script:Node -Storage $script:Storage ` + -Url $templateUrl -Filename 'alpine-3.20-default_20240908_amd64.tar.xz' ` + -ContentType 'vztmpl' -Wait -ErrorAction Stop + } catch { + # Template may already exist from a previous run or the Storage — Download context + } + } + $script:TestContainerId = $null + } + + It 'Should list containers (Get-PveContainer)' { + if (Skip-IfNoTarget) { return } + + # Zero containers is acceptable; just verify the cmdlet does not throw. + { Get-PveContainer -Node $script:Node -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should create a container (New-PveContainer)' { + if (Skip-IfNoTarget) { return } + + $securePassword = ConvertTo-SecureString 'Pester12345!' -AsPlainText -Force + + $task = New-PveContainer ` + -Node $script:Node ` + -Hostname 'pester-ct' ` + -Memory 128 ` + -Cores 1 ` + -RootFsStorage 'local-lvm' ` + -RootFsSize '1' ` + -OsTemplate "$($script:Storage):vztmpl/alpine-3.20-default_20240908_amd64.tar.xz" ` + -Password $securePassword ` + -Bridge 'vmbr0' ` + -Wait + + $task | Should -Not -BeNullOrEmpty + + # Retrieve the new container ID from the cluster + $ct = Get-PveContainer -Node $script:Node -Name 'pester-ct' | + Select-Object -First 1 + $ct | Should -Not -BeNullOrEmpty + + $script:TestContainerId = $ct.VmId + $script:CreatedContainerIds.Add($ct.VmId) + } + + It 'Should get container config (Get-PveContainerConfig)' { + if (Skip-IfNoTestContainer) { return } + + $config = Get-PveContainerConfig -Node $script:Node -VmId $script:TestContainerId + $config | Should -Not -BeNullOrEmpty + } + + It 'Should update container config (Set-PveContainerConfig)' { + if (Skip-IfNoTestContainer) { return } + + { Set-PveContainerConfig -Node $script:Node -VmId $script:TestContainerId ` + -Description 'Updated by Pester integration test' ` + -ErrorAction Stop } | Should -Not -Throw + + $config = Get-PveContainerConfig -Node $script:Node -VmId $script:TestContainerId + $config.Description | Should -Be 'Updated by Pester integration test' + } + + It 'Should start a container (Start-PveContainer)' { + if (Skip-IfNoTestContainer) { return } + + $task = Start-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait + $task | Should -Not -BeNullOrEmpty + + $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId + $ct.Status | Should -Be 'running' + } + + It 'Should stop a container (Stop-PveContainer)' { + if (Skip-IfNoTestContainer) { return } + + $task = Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Confirm:$false + $task | Should -Not -BeNullOrEmpty + + $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId + $ct.Status | Should -Be 'stopped' + } + + It 'Should restart a container (Restart-PveContainer)' { + if (Skip-IfNoTestContainer) { return } + + # Start first so we can restart + Start-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait | Out-Null + + $task = Restart-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait + $task | Should -Not -BeNullOrEmpty + + $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId + $ct.Status | Should -Be 'running' + + # Stop for subsequent tests + Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Confirm:$false | Out-Null + } + + It 'Should clone a container (Copy-PveContainer)' { + if (Skip-IfNoTestContainer) { return } + + $cloneId = $script:TestContainerId + 1000 + + $task = Copy-PveContainer ` + -SourceNode $script:Node ` + -VmId $script:TestContainerId ` + -NewVmId $cloneId ` + -NewName 'pester-clone-ct' ` + -Full ` + -Wait + + $task | Should -Not -BeNullOrEmpty + + $cloned = Get-PveContainer -Node $script:Node -Name 'pester-clone-ct' | + Select-Object -First 1 + $cloned | Should -Not -BeNullOrEmpty + + $script:CreatedContainerIds.Add($cloned.VmId) + } + } + + # ----------------------------------------------------------------------- + Context 'Container Snapshots' { + It 'Should create a container snapshot (New-PveContainerSnapshot)' { + if (Skip-IfNoTestContainer) { return } + + # Ensure container is stopped + $ct = Get-PveContainer -Node $script:Node -VmId $script:TestContainerId + if ($ct.Status -eq 'running') { + Stop-PveContainer -Node $script:Node -VmId $script:TestContainerId -Wait -Confirm:$false | Out-Null + } + + $task = New-PveContainerSnapshot ` + -Node $script:Node ` + -VmId $script:TestContainerId ` + -Name 'pester-ct-snap' ` + -Description 'Created by Pester integration test' ` + -Wait + + $task | Should -Not -BeNullOrEmpty + } + + It 'Should list container snapshots (Get-PveContainerSnapshot)' { + if (Skip-IfNoTestContainer) { return } + + $snapshots = Get-PveContainerSnapshot -Node $script:Node -VmId $script:TestContainerId + $snap = $snapshots | Where-Object { $_.Name -eq 'pester-ct-snap' } + $snap | Should -Not -BeNullOrEmpty + } + + It 'Should restore a container snapshot (Restore-PveContainerSnapshot)' { + if (Skip-IfNoTestContainer) { return } + + { Restore-PveContainerSnapshot ` + -Node $script:Node ` + -VmId $script:TestContainerId ` + -Name 'pester-ct-snap' ` + -Confirm:$false ` + -Wait } | Should -Not -Throw + } + + It 'Should remove a container snapshot (Remove-PveContainerSnapshot)' { + if (Skip-IfNoTestContainer) { return } + + $task = Remove-PveContainerSnapshot ` + -Node $script:Node ` + -VmId $script:TestContainerId ` + -Name 'pester-ct-snap' ` + -Confirm:$false ` + -Wait + + $task | Should -Not -BeNullOrEmpty + } } # ----------------------------------------------------------------------- @@ -798,6 +1191,28 @@ Describe 'Integration Tests' -Tag 'Integration' { } } + # ----------------------------------------------------------------------- + Context 'OVA Import' { + It 'Should import an OVA as a VM (Import-PveOva)' { + if (Skip-IfNoTarget) { return } + if (-not $script:OvaPath -or -not (Test-Path $script:OvaPath)) { + Set-ItResult -Skipped -Because 'PVETEST_OVA_PATH not set or file not found' + return + } + + $vm = Import-PveOva -Node $script:Node -Storage $script:Storage ` + -Path $script:OvaPath -TargetStorage 'local-lvm' ` + -Name 'pester-ova-vm' -Wait + + $vm | Should -Not -BeNullOrEmpty + $script:CreatedVmIds.Add($vm.VmId) + + # Verify the VM exists and has the right name + $found = Get-PveVm -Node $script:Node -Name 'pester-ova-vm' | Select-Object -First 1 + $found | Should -Not -BeNullOrEmpty + } + } + # ----------------------------------------------------------------------- Context 'Templates — Convert and Clone' { It 'Should convert the Linux VM to a template' { @@ -844,6 +1259,16 @@ Describe 'Integration Tests' -Tag 'Integration' { # Track for cleanup $script:CreatedVmIds.Add($cloned.VmId) } + + It 'Should remove a template (Remove-PveTemplate)' { + if (Skip-IfNoLinuxVm) { return } + + { Remove-PveTemplate -Node $script:Node -VmId $script:LinuxVmId ` + -Confirm:$false -ErrorAction Stop } | Should -Not -Throw + + $script:CreatedVmIds.Remove($script:LinuxVmId) | Out-Null + $script:LinuxVmId = $null + } } # ----------------------------------------------------------------------- @@ -869,4 +1294,19 @@ Describe 'Integration Tests' -Tag 'Integration' { $script:TestVmId = $null } } + + # ----------------------------------------------------------------------- + Context 'Disconnect' { + It 'Should disconnect from PVE server (Disconnect-PveServer)' { + if (Skip-IfNoTarget) { return } + + { Disconnect-PveServer -ErrorAction Stop } | Should -Not -Throw + } + + It 'Should fail commands after disconnect' { + if (Skip-IfNoTarget) { return } + + { Get-PveNode -ErrorAction Stop } | Should -Throw '*No active Proxmox VE session*' + } + } } diff --git a/tests/PSProxmoxVE.Tests/Vms/GuestAgentCmdlets.Tests.ps1 b/tests/PSProxmoxVE.Tests/Vms/GuestAgentCmdlets.Tests.ps1 new file mode 100644 index 0000000..1f10b31 --- /dev/null +++ b/tests/PSProxmoxVE.Tests/Vms/GuestAgentCmdlets.Tests.ps1 @@ -0,0 +1,214 @@ +#Requires -Module Pester +<# +.SYNOPSIS + Pester 5 tests for QEMU guest agent cmdlets: + Test-PveVmGuestAgent, Get-PveVmGuestNetwork, Invoke-PveVmGuestExec. + + All tests are fully offline — no live Proxmox VE target is required. + If a cmdlet is not yet compiled the test is marked Skipped. +#> + +BeforeAll { + . $PSScriptRoot/../_TestHelper.ps1 + + $script:Availability = @{} + foreach ($name in @('Test-PveVmGuestAgent', 'Get-PveVmGuestNetwork', 'Invoke-PveVmGuestExec')) { + $script:Availability[$name] = $null -ne (Get-Command $name -ErrorAction SilentlyContinue) + } + + function Skip-IfMissing([string]$Name) { + if (-not $script:Availability[$Name]) { + Set-ItResult -Skipped -Because "$Name is not yet implemented in this build" + } + } +} + +# --------------------------------------------------------------------------- +# Manifest contract +# --------------------------------------------------------------------------- +Describe 'Guest agent cmdlets — manifest declarations' { + BeforeAll { + $manifestPath = Join-Path (Get-Module PSProxmoxVE).ModuleBase 'PSProxmoxVE.psd1' + $script:Manifest = if (Test-Path $manifestPath) { Import-PowerShellDataFile $manifestPath } else { $null } + } + + It " should be declared in CmdletsToExport" -TestCases @( + @{ cmdName = 'Test-PveVmGuestAgent' } + @{ cmdName = 'Get-PveVmGuestNetwork' } + @{ cmdName = 'Invoke-PveVmGuestExec' } + ) { + if ($null -eq $script:Manifest) { Set-ItResult -Skipped -Because 'Manifest not found'; return } + $script:Manifest.CmdletsToExport | Should -Contain $cmdName + } +} + +# --------------------------------------------------------------------------- +# Test-PveVmGuestAgent +# --------------------------------------------------------------------------- +Describe 'Test-PveVmGuestAgent' { + + BeforeAll { $script:Cmd = Get-Command 'Test-PveVmGuestAgent' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Test-PveVmGuestAgent' + $script:Cmd | Should -Not -BeNullOrEmpty + } + + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Test-PveVmGuestAgent' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Test-PveVmGuestAgent' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + + It 'VmId should be Mandatory' { + Skip-IfMissing 'Test-PveVmGuestAgent' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + + It 'VmId should accept pipeline input by property name' { + Skip-IfMissing 'Test-PveVmGuestAgent' + $attr = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.ValueFromPipelineByPropertyName } + $attr | Should -Not -BeNullOrEmpty + } + + It 'Should output bool' { + Skip-IfMissing 'Test-PveVmGuestAgent' + $outputType = $script:Cmd.OutputType | Select-Object -First 1 + $outputType.Type | Should -Be ([bool]) + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Test-PveVmGuestAgent' + { Test-PveVmGuestAgent -Node 'pve1' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Get-PveVmGuestNetwork +# --------------------------------------------------------------------------- +Describe 'Get-PveVmGuestNetwork' { + + BeforeAll { $script:Cmd = Get-Command 'Get-PveVmGuestNetwork' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + $script:Cmd | Should -Not -BeNullOrEmpty + } + + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'Parameter metadata' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + + It 'VmId should be Mandatory' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + + It 'VmId should accept pipeline input by property name' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + $attr = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.ValueFromPipelineByPropertyName } + $attr | Should -Not -BeNullOrEmpty + } + + It 'Should have Session parameter' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + $script:Cmd.Parameters.ContainsKey('Session') | Should -BeTrue + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Get-PveVmGuestNetwork' + { Get-PveVmGuestNetwork -Node 'pve1' -VmId 100 -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} + +# --------------------------------------------------------------------------- +# Invoke-PveVmGuestExec +# --------------------------------------------------------------------------- +Describe 'Invoke-PveVmGuestExec' { + + BeforeAll { $script:Cmd = Get-Command 'Invoke-PveVmGuestExec' -ErrorAction SilentlyContinue } + + Context 'Command existence' { + It 'Should be available after module import' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + $script:Cmd | Should -Not -BeNullOrEmpty + } + + It 'Should be a CmdletInfo (binary cmdlet)' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + $script:Cmd.CommandType | Should -Be 'Cmdlet' + } + } + + Context 'ShouldProcess support' { + It 'Should support WhatIf' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + $script:Cmd.Parameters.ContainsKey('WhatIf') | Should -BeTrue + } + } + + Context 'Required parameters' { + It 'Node should be Mandatory' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + $isMandatory = $script:Cmd.Parameters['Node'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + + It 'VmId should be Mandatory' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + $isMandatory = $script:Cmd.Parameters['VmId'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + + It 'Command should be Mandatory' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + $isMandatory = $script:Cmd.Parameters['Command'].ParameterSets.Values | + Where-Object { $_.IsMandatory } + $isMandatory | Should -Not -BeNullOrEmpty + } + } + + Context 'Without active session' { + It 'Should throw when no session is active' { + Skip-IfMissing 'Invoke-PveVmGuestExec' + { Invoke-PveVmGuestExec -Node 'pve1' -VmId 100 -Command 'hostname' -Confirm:$false -ErrorAction Stop } | + Should -Throw '*No active Proxmox VE session*' + } + } +} diff --git a/tests/infrastructure/scripts/prepare-test-environment.sh b/tests/infrastructure/scripts/prepare-test-environment.sh index c4c51c6..014c21d 100755 --- a/tests/infrastructure/scripts/prepare-test-environment.sh +++ b/tests/infrastructure/scripts/prepare-test-environment.sh @@ -56,5 +56,92 @@ else echo "Cloud image already cached at ${CLOUD_IMAGE_PATH}" fi +# Create a minimal test OVA for Import-PveOva testing +# OVA = TAR containing an OVF descriptor + a small VMDK/raw disk +OVA_PATH="${OUTPUT_DIR}/test-appliance.ova" +if [ ! -f "${OVA_PATH}" ]; then + echo "Creating minimal test OVA..." + OVA_TMPDIR=$(mktemp -d) + + # Create a 1MB raw disk image and convert to vmdk-stream (flat) + dd if=/dev/zero of="${OVA_TMPDIR}/test-disk.vmdk" bs=1M count=1 2>/dev/null + + # Create OVF descriptor + cat > "${OVA_TMPDIR}/test-appliance.ovf" <<'OVF' + + + + + + + Virtual disk information + + + + A minimal test appliance + + Linux 64-bit + Linux + + + Virtual hardware requirements + + Virtual Hardware Family + 0 + test-appliance + vmx-13 + + + Number of Virtual CPUs + 1 virtual CPU(s) + 1 + 3 + 1 + + + byte * 2^20 + Memory Size + 256MB of memory + 2 + 4 + 256 + + + SCSI Controller + 3 + lsilogic + 6 + + + Hard Disk 1 + ovf:/disk/vmdisk1 + 4 + 3 + 17 + + + VM Network + Ethernet adapter 1 + 5 + E1000 + 10 + + + + +OVF + + # Pack as OVA (TAR, OVF first per spec) + (cd "${OVA_TMPDIR}" && tar cf "${OVA_PATH}" test-appliance.ovf test-disk.vmdk) + rm -rf "${OVA_TMPDIR}" + echo "Created test OVA at ${OVA_PATH} ($(du -h "${OVA_PATH}" | cut -f1))" +else + echo "Test OVA already cached at ${OVA_PATH}" +fi + echo "CLOUD_IMAGE_PATH=${CLOUD_IMAGE_PATH}" +echo "OVA_PATH=${OVA_PATH}" echo "Environment preparation complete."