Public/Test-NMMApiEndpoint.ps1
|
function Test-NMMApiEndpoint { <# .SYNOPSIS Tests NMM-PS functions against their API endpoints and swagger schemas. .DESCRIPTION Automated testing framework that validates: - NMM-PS function output against raw API responses - Response properties against swagger schema definitions - Detects API changes and schema mismatches Requires authentication via Connect-NMMApi before running. .PARAMETER FunctionName Name of a specific function to test (e.g., "Get-NMMHostPool"). .PARAMETER All Test all enabled endpoints in the configuration. .PARAMETER AccountId Account ID to use for testing endpoints that require it. .PARAMETER ValidateSchema Validate responses against swagger schema definitions. .PARAMETER ShowDiff Show detailed differences between function and raw API output. .PARAMETER ExportPath Path to export test results as JSON. .PARAMETER Quiet Suppress console output, only return results object. .EXAMPLE Test-NMMApiEndpoint -FunctionName "Get-NMMHostPool" -AccountId 67 .EXAMPLE Test-NMMApiEndpoint -All -AccountId 67 -ValidateSchema .EXAMPLE Test-NMMApiEndpoint -All -AccountId 67 -ExportPath "./test-results.json" #> [CmdletBinding(DefaultParameterSetName = 'Single')] param( [Parameter(Mandatory = $true, ParameterSetName = 'Single')] [string]$FunctionName, [Parameter(Mandatory = $true, ParameterSetName = 'All')] [switch]$All, [Parameter()] [int]$AccountId, [Parameter()] [switch]$ValidateSchema, [Parameter()] [switch]$ShowDiff, [Parameter()] [string]$ExportPath, [Parameter()] [switch]$Quiet ) # Verify authentication if (-not $script:CachedToken) { throw "Not authenticated. Run Connect-NMMApi first." } # Load endpoint configuration $configPath = Join-Path $PSScriptRoot ".." "Private" "Data" "TestEndpoints.json" if (-not (Test-Path $configPath)) { throw "TestEndpoints.json not found at: $configPath" } $config = Get-Content $configPath -Raw | ConvertFrom-Json # Determine which endpoints to test $endpointsToTest = @() if ($All) { $endpointsToTest = $config.endpoints.PSObject.Properties | Where-Object { $_.Value.enabled -eq $true } | ForEach-Object { $_.Name } } else { if (-not $config.endpoints.$FunctionName) { throw "Function '$FunctionName' not found in TestEndpoints.json" } $endpointsToTest = @($FunctionName) } # Results collection $results = [System.Collections.Generic.List[PSCustomObject]]::new() $totalTests = $endpointsToTest.Count $passCount = 0 $warnCount = 0 $failCount = 0 # Console output header if (-not $Quiet) { Write-Host "" Write-Host "Testing NMM API Endpoints..." -ForegroundColor Cyan Write-Host ("=" * 60) -ForegroundColor DarkGray Write-Host "" } $testIndex = 0 foreach ($funcName in $endpointsToTest) { $testIndex++ $endpointConfig = $config.endpoints.$funcName $startTime = Get-Date $testResult = [PSCustomObject]@{ FunctionName = $funcName Endpoint = $endpointConfig.endpoint Method = $endpointConfig.method ApiVersion = $endpointConfig.apiVersion Status = 'Unknown' SchemaValid = $null PropertyMatch = $null FunctionOutput = $null RawApiOutput = $null FunctionCount = 0 RawApiCount = 0 MissingProps = @() ExtraProps = @() ErrorMessage = $null Duration = $null } if (-not $Quiet) { Write-Host "[$testIndex/$totalTests] " -ForegroundColor DarkGray -NoNewline Write-Host $funcName -ForegroundColor White Write-Host " Endpoint: $($endpointConfig.endpoint -replace '\{accountId\}', $AccountId)" -ForegroundColor DarkGray } try { # Check required parameters $requiredParams = $endpointConfig.requiredParams if ($requiredParams -contains 'AccountId' -and -not $AccountId) { throw "AccountId is required for this endpoint" } # Build function parameters $funcParams = @{} if ($requiredParams -contains 'AccountId') { $funcParams['AccountId'] = $AccountId } # Handle endpoints that need additional context switch ($endpointConfig.requiresContext) { 'HostPool' { # Get first host pool for context $pools = (Get-NMMHostPool -AccountId $AccountId).HostPool if ($pools -and $pools.Count -gt 0) { $pool = $pools[0] $funcParams['SubscriptionId'] = $pool.subscription $funcParams['ResourceGroup'] = $pool.resourceGroup $funcParams['PoolName'] = $pool.hostPoolName } else { throw "No host pools available for context" } } 'Device' { # Get first device for context $devices = Get-NMMDevice -AccountId $AccountId if ($devices -and @($devices).Count -gt 0) { $funcParams['DeviceId'] = $devices[0].id } else { throw "No devices available for context" } } 'User' { # Get first user for context $users = Get-NMMUsers -AccountId $AccountId if ($users -and @($users).Count -gt 0) { $funcParams['UserId'] = $users[0].entraId } else { throw "No users available for context" } } 'DesktopImage' { # Get first desktop image for context $images = Get-NMMDesktopImage -AccountId $AccountId if ($images -and @($images).Count -gt 0) { $img = $images[0] $funcParams['SubscriptionId'] = $img.subscriptionId $funcParams['ResourceGroup'] = $img.resourceGroup $funcParams['ImageName'] = $img.name } else { throw "No desktop images available for context" } } 'ProtectedItem' { # Get first protected item for context $items = Get-NMMProtectedItem -AccountId $AccountId if ($items -and @($items).Count -gt 0) { $funcParams['ProtectedItemId'] = $items[0].protectedItemId } else { throw "No protected items available for context" } } 'Group' { # Groups require specific GroupId - skip for now throw "Group context requires specific GroupId - test manually" } 'Schedule' { # Get first schedule for context $schedules = Get-NMMSchedule -Scope Global if ($schedules -and @($schedules).Count -gt 0) { $funcParams['ScheduleId'] = $schedules[0].id $funcParams['Scope'] = 'Global' } else { throw "No schedules available for context" } } 'ScriptedAction' { # Get first scripted action for context $actions = Get-NMMScriptedAction -Scope Global if ($actions -and @($actions).Count -gt 0) { $funcParams['ScriptedActionId'] = $actions[0].id $funcParams['Scope'] = 'Global' } else { throw "No scripted actions available for context" } } } # Add any extra function parameters from config if ($endpointConfig.functionParams) { $endpointConfig.functionParams.PSObject.Properties | ForEach-Object { $value = $_.Value # Replace placeholder values with actual values if ($value -eq '{AccountId}') { $value = $AccountId } $funcParams[$_.Name] = $value } } # Call the NMM-PS function $funcOutput = & $funcName @funcParams $testResult.FunctionOutput = $funcOutput $testResult.FunctionCount = @($funcOutput).Count # Build raw API endpoint - replace all placeholders with actual values $rawEndpoint = $endpointConfig.endpoint -replace '\{accountId\}', $AccountId if ($funcParams.ContainsKey('SubscriptionId')) { $rawEndpoint = $rawEndpoint -replace '\{subscriptionId\}', $funcParams['SubscriptionId'] } if ($funcParams.ContainsKey('ResourceGroup')) { $rawEndpoint = $rawEndpoint -replace '\{resourceGroup\}', $funcParams['ResourceGroup'] } if ($funcParams.ContainsKey('PoolName')) { $rawEndpoint = $rawEndpoint -replace '\{poolName\}', $funcParams['PoolName'] } if ($funcParams.ContainsKey('DeviceId')) { $rawEndpoint = $rawEndpoint -replace '\{deviceId\}', $funcParams['DeviceId'] } if ($funcParams.ContainsKey('UserId')) { $rawEndpoint = $rawEndpoint -replace '\{userId\}', $funcParams['UserId'] } if ($funcParams.ContainsKey('ImageName')) { $rawEndpoint = $rawEndpoint -replace '\{imageName\}', $funcParams['ImageName'] } if ($funcParams.ContainsKey('ProtectedItemId')) { $rawEndpoint = $rawEndpoint -replace '\{protectedItemId\}', $funcParams['ProtectedItemId'] } if ($funcParams.ContainsKey('ScheduleId')) { $rawEndpoint = $rawEndpoint -replace '\{scheduleId\}', $funcParams['ScheduleId'] } if ($funcParams.ContainsKey('ScriptedActionId')) { $rawEndpoint = $rawEndpoint -replace '\{scriptedActionId\}', $funcParams['ScriptedActionId'] } # Call raw API $apiParams = @{ Method = $endpointConfig.method Endpoint = $rawEndpoint ApiVersion = $endpointConfig.apiVersion } if ($endpointConfig.requestBody) { $apiParams['Body'] = $endpointConfig.requestBody } $rawOutput = Invoke-APIRequest @apiParams # Handle response wrapper if ($endpointConfig.responseWrapper -and $rawOutput.$($endpointConfig.responseWrapper)) { $rawOutput = $rawOutput.$($endpointConfig.responseWrapper) } $testResult.RawApiOutput = $rawOutput $testResult.RawApiCount = @($rawOutput).Count if (-not $Quiet) { Write-Host " " -NoNewline Write-Host "[OK]" -ForegroundColor Green -NoNewline Write-Host " API Call: $($testResult.RawApiCount) items" -ForegroundColor Gray } # Compare outputs $comparison = Compare-ApiResponse -FunctionOutput $funcOutput -RawApiOutput $rawOutput $testResult.PropertyMatch = $comparison.AreEqual if ($comparison.Differences.Count -gt 0) { $testResult.MissingProps = @($comparison.Differences | Where-Object { $_.Type -eq 'MissingInFunction' }).Path $testResult.ExtraProps = @($comparison.Differences | Where-Object { $_.Type -eq 'ExtraInFunction' }).Path } # Schema validation if ($ValidateSchema) { $swaggerSchema = Get-SwaggerSchema -SwaggerPath $endpointConfig.swaggerPath -Method $endpointConfig.method -ApiVersion $endpointConfig.apiVersion $schemaResult = Test-ResponseSchema -Response $rawOutput -SwaggerSchema $swaggerSchema -ApiVersion $endpointConfig.apiVersion $testResult.SchemaValid = $schemaResult.IsValid if (-not $Quiet) { if ($schemaResult.IsValid) { Write-Host " " -NoNewline Write-Host "[OK]" -ForegroundColor Green -NoNewline Write-Host " Schema: $($schemaResult.MatchingProps.Count)/$($schemaResult.TotalExpected) properties match" -ForegroundColor Gray } else { Write-Host " " -NoNewline Write-Host "[WARN]" -ForegroundColor Yellow -NoNewline Write-Host " Schema: $($schemaResult.Message)" -ForegroundColor Gray } } } # Determine status if ($testResult.FunctionCount -eq $testResult.RawApiCount -and $testResult.PropertyMatch) { $testResult.Status = 'Pass' $passCount++ } elseif ($testResult.MissingProps.Count -gt 0) { $testResult.Status = 'Warning' $warnCount++ } else { $testResult.Status = 'Pass' $passCount++ } if (-not $Quiet) { Write-Host " " -NoNewline Write-Host "[OK]" -ForegroundColor Green -NoNewline Write-Host " Function: $($testResult.FunctionCount) items returned" -ForegroundColor Gray $statusColor = switch ($testResult.Status) { 'Pass' { 'Green' } 'Warning' { 'Yellow' } 'Fail' { 'Red' } default { 'Gray' } } Write-Host " Status: " -NoNewline Write-Host $testResult.Status.ToUpper() -ForegroundColor $statusColor } # Show diff if requested if ($ShowDiff -and $comparison.Differences.Count -gt 0) { Write-Host "" Write-Host " Differences:" -ForegroundColor Yellow foreach ($diff in $comparison.Differences) { Write-Host " - $($diff.Path): $($diff.Type)" -ForegroundColor DarkGray } } } catch { $testResult.Status = 'Fail' $testResult.ErrorMessage = $_.Exception.Message $failCount++ if (-not $Quiet) { Write-Host " " -NoNewline Write-Host "[FAIL]" -ForegroundColor Red -NoNewline Write-Host " $($_.Exception.Message)" -ForegroundColor Gray Write-Host " Status: " -NoNewline Write-Host "FAIL" -ForegroundColor Red } } $testResult.Duration = (Get-Date) - $startTime $results.Add($testResult) if (-not $Quiet) { Write-Host "" } } # Summary if (-not $Quiet) { Write-Host ("=" * 60) -ForegroundColor DarkGray Write-Host "Summary: " -NoNewline Write-Host "$passCount Pass" -ForegroundColor Green -NoNewline Write-Host " | " -NoNewline Write-Host "$warnCount Warning" -ForegroundColor Yellow -NoNewline Write-Host " | " -NoNewline Write-Host "$failCount Fail" -ForegroundColor Red Write-Host ("=" * 60) -ForegroundColor DarkGray } # Export to JSON if requested if ($ExportPath) { $exportData = @{ TestRun = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' TotalTests = $totalTests PassCount = $passCount WarningCount = $warnCount FailCount = $failCount Results = $results | ForEach-Object { @{ FunctionName = $_.FunctionName Endpoint = $_.Endpoint Method = $_.Method ApiVersion = $_.ApiVersion Status = $_.Status SchemaValid = $_.SchemaValid PropertyMatch = $_.PropertyMatch FunctionCount = $_.FunctionCount RawApiCount = $_.RawApiCount MissingProps = $_.MissingProps ExtraProps = $_.ExtraProps ErrorMessage = $_.ErrorMessage DurationMs = $_.Duration.TotalMilliseconds } } } $exportData | ConvertTo-Json -Depth 10 | Out-File -FilePath $ExportPath -Encoding UTF8 Write-Host "" Write-Host "Results exported to: $ExportPath" -ForegroundColor Cyan } return $results.ToArray() } |