From e2b471b9ac4d670c956035beccfe3ae5621218b5 Mon Sep 17 00:00:00 2001 From: Clint Branham Date: Tue, 17 Mar 2026 16:15:20 -0500 Subject: [PATCH] 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) --- .github/workflows/mock-integration-tests.yml | 81 +++++ .../MockIntegration/MockServer.Tests.ps1 | 308 ++++++++++++++++++ 2 files changed, 389 insertions(+) create mode 100644 .github/workflows/mock-integration-tests.yml create mode 100644 tests/PSProxmoxVE.Tests/MockIntegration/MockServer.Tests.ps1 diff --git a/.github/workflows/mock-integration-tests.yml b/.github/workflows/mock-integration-tests.yml new file mode 100644 index 0000000..e924e0c --- /dev/null +++ b/.github/workflows/mock-integration-tests.yml @@ -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/ diff --git a/tests/PSProxmoxVE.Tests/MockIntegration/MockServer.Tests.ps1 b/tests/PSProxmoxVE.Tests/MockIntegration/MockServer.Tests.ps1 new file mode 100644 index 0000000..571536c --- /dev/null +++ b/tests/PSProxmoxVE.Tests/MockIntegration/MockServer.Tests.ps1 @@ -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 + } +}