mirror of
https://github.com/GoodOlClint/PSProxmoxVE.git
synced 2026-08-10 06:16:53 +00:00
test: expand integration and unit test coverage to 84 test cases
Integration tests (27 contexts, 84 test cases, up from 48 cmdlets to 76): - VM: Suspend/Resume/Resize - Containers: full lifecycle (create, config, start, stop, restart, clone) - Container Snapshots: create, list, restore, remove - SDN: zone/vnet/subnet CRUD (PVE 8+ version-gated) - Storage: create/remove directory storage, Invoke-PveStorageDownload - OVA Import: Import-PveOva with generated test OVA - Cloud-Init: Invoke-PveCloudInitRegenerate - Templates: Remove-PveTemplate - Disconnect-PveServer: verify disconnect + post-disconnect failure Unit tests: - Guest agent cmdlets (Test-PveVmGuestAgent, Get-PveVmGuestNetwork, Invoke-PveVmGuestExec) — parameter metadata, ShouldProcess, session Test infrastructure: - prepare-test-environment.sh generates minimal test OVA (OVF + 1MB VMDK) - CI workflow passes PVETEST_OVA_PATH to integration tests Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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*'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 "<cmdName> 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*'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1"
|
||||
xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"
|
||||
xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"
|
||||
xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1">
|
||||
<References>
|
||||
<File ovf:id="file1" ovf:href="test-disk.vmdk" ovf:size="1048576"/>
|
||||
</References>
|
||||
<DiskSection>
|
||||
<Info>Virtual disk information</Info>
|
||||
<Disk ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:capacity="1073741824" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"/>
|
||||
</DiskSection>
|
||||
<VirtualSystem ovf:id="test-appliance">
|
||||
<Info>A minimal test appliance</Info>
|
||||
<OperatingSystemSection ovf:id="101">
|
||||
<Info>Linux 64-bit</Info>
|
||||
<Description>Linux</Description>
|
||||
</OperatingSystemSection>
|
||||
<VirtualHardwareSection>
|
||||
<Info>Virtual hardware requirements</Info>
|
||||
<System>
|
||||
<vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
|
||||
<vssd:InstanceID>0</vssd:InstanceID>
|
||||
<vssd:VirtualSystemIdentifier>test-appliance</vssd:VirtualSystemIdentifier>
|
||||
<vssd:VirtualSystemType>vmx-13</vssd:VirtualSystemType>
|
||||
</System>
|
||||
<Item>
|
||||
<rasd:Description>Number of Virtual CPUs</rasd:Description>
|
||||
<rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
|
||||
<rasd:InstanceID>1</rasd:InstanceID>
|
||||
<rasd:ResourceType>3</rasd:ResourceType>
|
||||
<rasd:VirtualQuantity>1</rasd:VirtualQuantity>
|
||||
</Item>
|
||||
<Item>
|
||||
<rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
|
||||
<rasd:Description>Memory Size</rasd:Description>
|
||||
<rasd:ElementName>256MB of memory</rasd:ElementName>
|
||||
<rasd:InstanceID>2</rasd:InstanceID>
|
||||
<rasd:ResourceType>4</rasd:ResourceType>
|
||||
<rasd:VirtualQuantity>256</rasd:VirtualQuantity>
|
||||
</Item>
|
||||
<Item>
|
||||
<rasd:ElementName>SCSI Controller</rasd:ElementName>
|
||||
<rasd:InstanceID>3</rasd:InstanceID>
|
||||
<rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>
|
||||
<rasd:ResourceType>6</rasd:ResourceType>
|
||||
</Item>
|
||||
<Item>
|
||||
<rasd:ElementName>Hard Disk 1</rasd:ElementName>
|
||||
<rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
|
||||
<rasd:InstanceID>4</rasd:InstanceID>
|
||||
<rasd:Parent>3</rasd:Parent>
|
||||
<rasd:ResourceType>17</rasd:ResourceType>
|
||||
</Item>
|
||||
<Item>
|
||||
<rasd:Connection>VM Network</rasd:Connection>
|
||||
<rasd:ElementName>Ethernet adapter 1</rasd:ElementName>
|
||||
<rasd:InstanceID>5</rasd:InstanceID>
|
||||
<rasd:ResourceSubType>E1000</rasd:ResourceSubType>
|
||||
<rasd:ResourceType>10</rasd:ResourceType>
|
||||
</Item>
|
||||
</VirtualHardwareSection>
|
||||
</VirtualSystem>
|
||||
</Envelope>
|
||||
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."
|
||||
|
||||
Reference in New Issue
Block a user