test(mock): add mock integration tests and CI workflow

Pester tests that start the mock PVE server, run real cmdlets against it,
and validate end-to-end behavior: auth flows, node/VM/storage/network/
user operations, pipeline support, and deserialization. Runs on every
push via GitHub-hosted runners.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Clint Branham
2026-03-17 16:15:20 -05:00
parent 239aee630f
commit e2b471b9ac
2 changed files with 389 additions and 0 deletions
@@ -0,0 +1,81 @@
name: Mock Integration Tests
# These tests run the module against a mock PVE API server.
# No real Proxmox host needed - runs on GitHub-hosted runners.
# Validates HTTP request correctness, auth flows, deserialization,
# and end-to-end cmdlet behavior against simulated PVE responses.
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
mock-integration:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
framework: net10.0
shell: pwsh
- os: windows-latest
framework: net10.0
shell: pwsh
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Build mock server
run: dotnet build tests/PSProxmoxVE.MockServer/PSProxmoxVE.MockServer.csproj --configuration Release
- name: Build module
run: dotnet publish src/PSProxmoxVE/PSProxmoxVE.csproj --configuration Release --framework net10.0 --output ./publish/net10.0
- name: Install Pester 5
shell: pwsh
run: Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser
- name: Copy module to module path (Windows)
if: matrix.os == 'windows-latest'
shell: pwsh
run: |
$modulePath = "$env:USERPROFILE\Documents\PowerShell\Modules\PSProxmoxVE"
New-Item -ItemType Directory -Path $modulePath -Force | Out-Null
Copy-Item -Path .\publish\net10.0\* -Destination $modulePath -Recurse -Force
- name: Copy module to module path (Linux)
if: matrix.os != 'windows-latest'
shell: pwsh
run: |
$modulePath = "$HOME/.local/share/powershell/Modules/PSProxmoxVE"
New-Item -ItemType Directory -Path $modulePath -Force | Out-Null
Copy-Item -Path ./publish/net10.0/* -Destination $modulePath -Recurse -Force
- name: Run mock integration tests
shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0
$config = New-PesterConfiguration
$config.Run.Path = "tests/PSProxmoxVE.Tests/MockIntegration"
$config.Filter.Tag = "MockIntegration"
$config.Output.Verbosity = "Detailed"
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = "NUnitXml"
$config.TestResult.OutputPath = "TestResults/mock-integration-results.xml"
Invoke-Pester -Configuration $config
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: mock-integration-results-${{ matrix.os }}
path: TestResults/
@@ -0,0 +1,308 @@
#Requires -Modules Pester
<#
.SYNOPSIS
Integration tests that run PSProxmoxVE cmdlets against the mock PVE API server.
These validate end-to-end behavior: HTTP requests, auth, deserialization, pipeline.
.DESCRIPTION
The mock server is started before all tests and stopped after. Each test group
exercises real cmdlet execution against simulated PVE API responses. No real
Proxmox host is needed.
#>
BeforeAll {
# Find the built mock server
$mockServerProject = Join-Path $PSScriptRoot '../../PSProxmoxVE.MockServer/PSProxmoxVE.MockServer.csproj'
if (-not (Test-Path $mockServerProject)) {
throw "Mock server project not found at $mockServerProject"
}
# Find the built module DLL
$dllSearchPaths = @(
(Join-Path $PSScriptRoot '../../PSProxmoxVE.MockServer/bin/Release/net10.0/PSProxmoxVE.MockServer.dll'),
(Join-Path $PSScriptRoot '../../PSProxmoxVE.MockServer/bin/Debug/net10.0/PSProxmoxVE.MockServer.dll')
)
$mockServerDll = $dllSearchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $mockServerDll) {
# Build it
$buildOutput = dotnet build $mockServerProject --configuration Release 2>&1
$mockServerDll = $dllSearchPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $mockServerDll) {
throw "Failed to build mock server: $buildOutput"
}
}
# Start mock server as a background process on a random port
$script:MockPort = Get-Random -Minimum 18000 -Maximum 19000
$script:MockHost = "localhost"
$script:MockUrl = "https://${script:MockHost}:${script:MockPort}"
$mockServerDir = Split-Path $mockServerDll -Parent
$script:MockProcess = Start-Process -FilePath 'dotnet' `
-ArgumentList @($mockServerDll, '--urls', "https://0.0.0.0:$($script:MockPort)") `
-WorkingDirectory $mockServerDir `
-PassThru -NoNewWindow -RedirectStandardOutput (Join-Path $TestDrive 'mock-stdout.log') `
-RedirectStandardError (Join-Path $TestDrive 'mock-stderr.log')
# Wait for server to become responsive
$maxWait = 30
$waited = 0
$ready = $false
while ($waited -lt $maxWait -and -not $ready) {
Start-Sleep -Seconds 1
$waited++
try {
# Use .NET directly to skip cert validation
$handler = [System.Net.Http.HttpClientHandler]::new()
$handler.ServerCertificateCustomValidationCallback = { $true }
$client = [System.Net.Http.HttpClient]::new($handler)
$result = $client.GetStringAsync("$($script:MockUrl)/api2/json/version").GetAwaiter().GetResult()
if ($result -match '"version"') {
$ready = $true
}
$client.Dispose()
$handler.Dispose()
} catch {
# Not ready yet
}
}
if (-not $ready) {
$stderr = if (Test-Path (Join-Path $TestDrive 'mock-stderr.log')) { Get-Content (Join-Path $TestDrive 'mock-stderr.log') -Raw } else { 'N/A' }
throw "Mock server failed to start within ${maxWait}s. Stderr: $stderr"
}
# Import the module
Import-Module PSProxmoxVE -Force -ErrorAction Stop
}
AfterAll {
# Stop mock server
if ($script:MockProcess -and -not $script:MockProcess.HasExited) {
$script:MockProcess.Kill()
$script:MockProcess.WaitForExit(5000)
}
# Disconnect any active session
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
Describe 'Mock PVE Server - Authentication' -Tag 'MockIntegration' {
It 'Should connect with API token' {
$session = Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck -PassThru
$session | Should -Not -BeNullOrEmpty
$session.Hostname | Should -Be $script:MockHost
$session.Port | Should -Be $script:MockPort
$session.AuthMode | Should -Be 'ApiToken'
$session.ServerVersion | Should -Not -BeNullOrEmpty
}
It 'Should connect with credentials' {
$secPassword = ConvertTo-SecureString 'testpass' -AsPlainText -Force
$cred = [PSCredential]::new('root@pam', $secPassword)
$session = Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-Credential $cred -SkipCertificateCheck -PassThru
$session | Should -Not -BeNullOrEmpty
$session.AuthMode | Should -Be 'Ticket'
$session.Ticket | Should -Not -BeNullOrEmpty
$session.CsrfToken | Should -Not -BeNullOrEmpty
$session.IsExpired | Should -Be $false
}
It 'Should report connection status' {
# Connect first
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
$result = Test-PveConnection
$result | Should -Be $true
}
It 'Should disconnect cleanly' {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
{ Disconnect-PveServer } | Should -Not -Throw
$result = Test-PveConnection
$result | Should -Be $false
}
}
Describe 'Mock PVE Server - Node Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list nodes' {
$nodes = Get-PveNode
$nodes | Should -Not -BeNullOrEmpty
$nodes[0].Node | Should -Not -BeNullOrEmpty
$nodes[0].Status | Should -Not -BeNullOrEmpty
}
It 'Should filter nodes by name' {
$nodes = Get-PveNode -Name 'pve1'
$nodes | Should -Not -BeNullOrEmpty
$nodes | ForEach-Object { $_.Node | Should -Be 'pve1' }
}
}
Describe 'Mock PVE Server - VM Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list VMs' {
$vms = Get-PveVm -Node 'pve1'
$vms | Should -Not -BeNullOrEmpty
$vms.Count | Should -BeGreaterOrEqual 1
}
It 'Should filter VMs by status' {
$running = Get-PveVm -Node 'pve1' -Status 'running'
$running | Should -Not -BeNullOrEmpty
$running | ForEach-Object { $_.Status | Should -Be 'running' }
}
It 'Should get VM config' {
$config = Get-PveVmConfig -Node 'pve1' -VmId 100
$config | Should -Not -BeNullOrEmpty
$config.Cores | Should -BeGreaterThan 0
}
It 'Should get VM config via pipeline' {
$vms = Get-PveVm -Node 'pve1' -VmId 100
$config = $vms | Get-PveVmConfig
$config | Should -Not -BeNullOrEmpty
}
}
Describe 'Mock PVE Server - Storage Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list storage' {
$storage = Get-PveStorage
$storage | Should -Not -BeNullOrEmpty
$storage[0].Storage | Should -Not -BeNullOrEmpty
}
It 'Should list storage content' {
$content = Get-PveStorageContent -Node 'pve1' -Storage 'local'
$content | Should -Not -BeNullOrEmpty
}
}
Describe 'Mock PVE Server - Network Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list networks' {
$networks = Get-PveNetwork -Node 'pve1'
$networks | Should -Not -BeNullOrEmpty
$networks[0].Iface | Should -Not -BeNullOrEmpty
}
}
Describe 'Mock PVE Server - User Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list users' {
$users = Get-PveUser
$users | Should -Not -BeNullOrEmpty
$users[0].UserId | Should -Not -BeNullOrEmpty
}
It 'Should list roles' {
$roles = Get-PveRole
$roles | Should -Not -BeNullOrEmpty
$roles[0].RoleId | Should -Not -BeNullOrEmpty
}
}
Describe 'Mock PVE Server - Cluster Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list snapshots' {
$snapshots = Get-PveSnapshot -Node 'pve1' -VmId 100
$snapshots | Should -Not -BeNullOrEmpty
}
}
Describe 'Mock PVE Server - Container Operations' -Tag 'MockIntegration' {
BeforeAll {
Connect-PveServer -Server $script:MockHost -Port $script:MockPort `
-ApiToken 'root@pam!test=00000000-0000-0000-0000-000000000000' `
-SkipCertificateCheck
}
AfterAll {
try { Disconnect-PveServer -ErrorAction SilentlyContinue } catch { }
}
It 'Should list containers' {
$containers = Get-PveContainer -Node 'pve1'
$containers | Should -Not -BeNullOrEmpty
$containers[0].VmId | Should -BeGreaterThan 0
}
}