param( [string]$ApiBaseUrl = 'http://localhost:5286/api', [string]$ClientId = 'ssp-demo-worker', [string]$ClientSecret = 'DemoOnly-DoNotUseInProduction!', [int]$ApiTimeoutSec = 30 ) $ErrorActionPreference = 'Stop' function Invoke-Api { param( [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] [string]$Method, [string]$Path, [object]$Body, [string]$BearerToken, [switch]$AllowNotFound ) $uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/')) $parameters = @{ Method = $Method Uri = $uri TimeoutSec = $ApiTimeoutSec ErrorAction = 'Stop' } if ($Body) { $parameters.Body = ($Body | ConvertTo-Json -Depth 20) $parameters.ContentType = 'application/json' } if ($BearerToken) { $parameters.Headers = @{ Authorization = "Bearer $BearerToken" } } try { Invoke-RestMethod @parameters } catch { $statusCode = $null if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { $statusCode = [int]$_.Exception.Response.StatusCode } if ($AllowNotFound -and $statusCode -eq 404) { return $null } $responseBody = '' if ($_.Exception.Response) { try { $stream = $_.Exception.Response.GetResponseStream() if ($stream) { $reader = [System.IO.StreamReader]::new($stream) $text = $reader.ReadToEnd() if (-not [string]::IsNullOrWhiteSpace($text)) { $responseBody = $text } } } catch { $responseBody = '' } } throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)" } } function Get-AuthToken { param([string]$Scope) $token = Invoke-Api -Method POST -Path 'auth/token' -Body @{ clientId = $ClientId clientSecret = $ClientSecret scope = $Scope } return $token.accessToken } function Find-ByName { param( [object[]]$Items, [string]$Name ) return @($Items | Where-Object { $_.name -eq $Name })[0] } function ConvertTo-GuidValue { param([object]$Value) return [guid]@($Value)[0] } function ConvertTo-ApiItems { param([object]$Value) if ($null -eq $Value) { return @() } $items = @($Value) if ($items.Count -eq 1 -and $items[0] -is [array]) { return @($items[0]) } if ($items.Count -eq 1 -and $items[0].PSObject.Properties['value']) { return @($items[0].value) } return $items } function Write-TestSection { param([string]$Name) Write-Host "" Write-Host ("=== {0} ===" -f $Name) -ForegroundColor Cyan } function Write-TestInfo { param( [string]$Label, [object]$Value ) Write-Host (" - {0}: {1}" -f $Label, $Value) } Describe 'API controller workflow smoke test' { $script:token = $null $script:environmentId = $null $script:domainIds = @() $script:targetIds = @() $script:deploymentBatchId = $null $script:templateSelectionIds = @() $script:targetAssignmentIds = @() $script:configurationValueId = $null $script:credentialSecretId = $null $script:testRunId = ([guid]::NewGuid().ToString('N')).Substring(0, 8) $script:environmentName = "Pester Environment $script:testRunId" $script:domainName1 = "Pester Domain A $script:testRunId" $script:domainName2 = "Pester Domain B $script:testRunId" $script:targetName1 = "PST-$script:testRunId-01" $script:targetName2 = "PST-$script:testRunId-02" $script:targetName3 = "PST-$script:testRunId-03" $script:credentialSecretName = "Pester/Workflow/$script:testRunId" try { It 'creates an environment, links two domains, creates targets and composes a deployment batch' { Write-TestSection -Name 'test run' Write-TestInfo -Label 'API base URL' -Value $ApiBaseUrl Write-TestInfo -Label 'Run id' -Value $script:testRunId Write-TestSection -Name 'auth and inventory' $script:token = Get-AuthToken -Scope 'configuration.read configuration.write credential.read credential.write credential.resolve deployment.read deployment.write queue.read template.read token.manage token.admin' Write-TestInfo -Label 'Bearer token' -Value 'issued' $allowedScopesResponse = Invoke-Api -Method GET -Path 'tokens/scopes' -BearerToken $script:token $allowedScopes = @($allowedScopesResponse) if ($allowedScopesResponse.PSObject.Properties['value']) { $allowedScopes = @($allowedScopesResponse.value) } $allowedScopes -contains 'deployment.write' | Should Be $true Write-TestInfo -Label 'Allowed scopes' -Value @($allowedScopes).Count $myTokens = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'tokens/my' -BearerToken $script:token)) $myTokens.Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Managed tokens visible' -Value $myTokens.Count $services = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Service' -BearerToken $script:token)) $services.Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Services' -Value $services.Count $service = $services | Select-Object -First 1 Invoke-Api -Method GET -Path ("Service/{0}" -f $service.id) -BearerToken $script:token | Should Not BeNullOrEmpty Invoke-Api -Method GET -Path ("Service/{0}/RoleDefinitions" -f $service.id) -BearerToken $script:token | Out-Null $templateCategories = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'TemplateCategory' -BearerToken $script:token)) $templateCategories.Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Template categories' -Value $templateCategories.Count Invoke-Api -Method GET -Path ("TemplateCategory/{0}" -f $templateCategories[0].id) -BearerToken $script:token | Should Not BeNullOrEmpty $deploymentRules = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'deployment-rules' -BearerToken $script:token)) $deploymentRules.Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Deployment rules' -Value $deploymentRules.Count Invoke-Api -Method GET -Path ("deployment-rules/{0}" -f $deploymentRules[0].id) -BearerToken $script:token | Should Not BeNullOrEmpty $configurationDefinitions = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'configuration-definitions' -BearerToken $script:token)) $configurationDefinitions.Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Configuration definitions' -Value $configurationDefinitions.Count $configurationDefinition = $configurationDefinitions | Select-Object -First 1 Invoke-Api -Method GET -Path ("configuration-definitions/{0}" -f $configurationDefinition.id) -BearerToken $script:token | Should Not BeNullOrEmpty Invoke-Api -Method GET -Path ("configuration-definitions/{0}/values" -f $configurationDefinition.id) -BearerToken $script:token | Out-Null Write-TestSection -Name 'credential secret' $script:credentialSecretId = ConvertTo-GuidValue -Value (Invoke-Api -Method POST -Path 'credential-secrets' -BearerToken $script:token -Body @{ name = $script:credentialSecretName userName = "CONTOSO\svc-pester-$script:testRunId" secretValue = 'PesterSecret!' secretType = 'Credential' metadataJson = (@{ source = 'Api.Workflow.Tests.ps1'; runId = $script:testRunId } | ConvertTo-Json -Compress) isEnabled = $true }) Write-TestInfo -Label 'Created credential secret' -Value $script:credentialSecretId Invoke-Api -Method GET -Path ("credential-secrets/{0}" -f $script:credentialSecretId) -BearerToken $script:token | Should Not BeNullOrEmpty $resolvedSecret = Invoke-Api -Method GET -Path ("credential-secrets/resolve?name={0}" -f [uri]::EscapeDataString($script:credentialSecretName)) -BearerToken $script:token $resolvedSecret.userName | Should Be "CONTOSO\svc-pester-$script:testRunId" Write-TestInfo -Label 'Resolved credential user' -Value $resolvedSecret.userName Write-TestSection -Name 'environment and domains' Invoke-Api -Method POST -Path 'Environment' -BearerToken $script:token -Body @{ name = $script:environmentName environmentType = 'Test' hostingType = 'OnPrem' providerType = 'Pester' metadataJson = (@{ source = 'Api.Workflow.Tests.ps1'; runId = $script:testRunId } | ConvertTo-Json -Compress) } | Out-Null $environment = Find-ByName -Items @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Environment' -BearerToken $script:token)) -Name $script:environmentName $environment | Should Not BeNullOrEmpty $script:environmentId = ConvertTo-GuidValue -Value $environment.id Write-TestInfo -Label 'Created environment' -Value ("{0} ({1})" -f $script:environmentName, $script:environmentId) $script:configurationValueId = ConvertTo-GuidValue -Value (Invoke-Api -Method POST -Path 'configuration-values' -BearerToken $script:token -Body @{ configurationDefinitionId = [string](ConvertTo-GuidValue -Value $configurationDefinition.id) scopeType = 'Environment' scopeId = [string]$script:environmentId valueSourceType = 'Static' valueJson = (@{ value = "Pester-$script:testRunId" } | ConvertTo-Json -Compress) sortOrder = 999 }) @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path ("configuration-values?scopeType=Environment&scopeId={0}" -f $script:environmentId) -BearerToken $script:token)).Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Created configuration value' -Value $script:configurationValueId $domainId1 = Invoke-Api -Method POST -Path 'Domain' -BearerToken $script:token -Body @{ name = $script:domainName1 fqdn = "pester-a-$script:testRunId.contoso.local" netBIOS = "PSTA$($script:testRunId.Substring(0, 4))".ToUpper() } $domainId2 = Invoke-Api -Method POST -Path 'Domain' -BearerToken $script:token -Body @{ name = $script:domainName2 fqdn = "pester-b-$script:testRunId.contoso.local" netBIOS = "PSTB$($script:testRunId.Substring(0, 4))".ToUpper() } $script:domainIds = @( (ConvertTo-GuidValue -Value $domainId1), (ConvertTo-GuidValue -Value $domainId2) ) Write-TestInfo -Label 'Created domains' -Value ($script:domainIds -join ', ') foreach ($domainId in $script:domainIds) { Invoke-Api -Method POST -Path ("Domain/{0}/Environment/{1}" -f $domainId, $script:environmentId) -BearerToken $script:token | Out-Null } Write-TestInfo -Label 'Linked domains to environment' -Value $script:domainIds.Count Write-TestSection -Name 'targets' $targetDefinitions = @( @{ name = $script:targetName1; domainId = $script:domainIds[0]; role = 'WebFrontEnd'; order = 10 }, @{ name = $script:targetName2; domainId = $script:domainIds[0]; role = 'Application'; order = 20 }, @{ name = $script:targetName3; domainId = $script:domainIds[1]; role = 'Search'; order = 30 } ) foreach ($targetDefinition in $targetDefinitions) { $targetId = Invoke-Api -Method POST -Path 'Target' -BearerToken $script:token -Body @{ domainID = $targetDefinition.domainId name = $targetDefinition.name targetType = 'VirtualMachine' providerType = 'OnPrem' externalId = $targetDefinition.name metadataJson = (@{ source = 'Api.Workflow.Tests.ps1'; role = $targetDefinition.role } | ConvertTo-Json -Compress) } $script:targetIds += ConvertTo-GuidValue -Value $targetId Write-TestInfo -Label 'Created target' -Value ("{0} / {1} ({2})" -f $targetDefinition.name, $targetDefinition.role, $targetId) } @($script:targetIds).Count | Should Be 3 Write-TestSection -Name 'template selection' $templates = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Template' -BearerToken $script:token)) $onPremServiceIds = @( $services | Where-Object { -not $_.isCloudService } | ForEach-Object { [string]$_.id } ) $onPremCategoryIds = @( $templateCategories | Where-Object { $onPremServiceIds -contains [string]$_.serviceId } | ForEach-Object { [string]$_.id } ) $selectedTemplates = @( $templates | Where-Object { $onPremCategoryIds -contains [string]$_.templateCategoryId } | Select-Object -First 2 ) @($selectedTemplates).Count | Should BeGreaterThan 0 Write-TestInfo -Label 'Available templates' -Value $templates.Count Write-TestInfo -Label 'Selected templates' -Value (($selectedTemplates | ForEach-Object { $_.name }) -join ', ') $templateSelections = @() $sortOrder = 10 foreach ($template in $selectedTemplates) { $versions = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path ("Template/{0}/Versions" -f $template.id) -BearerToken $script:token)) $version = $versions | Select-Object -First 1 $version | Should Not BeNullOrEmpty $templateSelections += @{ templateVersionId = [string](ConvertTo-GuidValue -Value $version.id) templateRole = 'Service' sortOrder = $sortOrder alias = "Pester-$sortOrder" } Write-TestInfo -Label 'Pinned template version' -Value ("{0} -> {1}" -f $template.name, $version.id) $sortOrder += 10 } $targetAssignments = @() foreach ($targetDefinition in $targetDefinitions) { $targetId = $script:targetIds[$targetDefinitions.IndexOf($targetDefinition)] $targetAssignments += @{ targetId = [string]$targetId roleKey = $targetDefinition.role sortOrder = $targetDefinition.order nodeDataJson = (@{ nodeName = $targetDefinition.name; pester = $true } | ConvertTo-Json -Compress) } } Write-TestSection -Name 'deployment batch' $script:deploymentBatchId = ConvertTo-GuidValue -Value (Invoke-Api -Method POST -Path 'deployment-batches' -BearerToken $script:token -Body @{ status = 'Pending' targetIds = @() templateSelections = $templateSelections targetAssignments = $targetAssignments }) $script:deploymentBatchId | Should Not BeNullOrEmpty Write-TestInfo -Label 'Created deployment batch' -Value $script:deploymentBatchId $composition = Invoke-Api -Method GET -Path ("deployment-batches/{0}/composition" -f $script:deploymentBatchId) -BearerToken $script:token @($composition.templateSelections).Count | Should Be @($templateSelections).Count $composition = Invoke-Api -Method GET -Path ("deployment-batches/{0}/composition" -f $script:deploymentBatchId) -BearerToken $script:token @($composition.targetAssignments).Count | Should Be 3 $script:targetAssignmentIds = @($composition.targetAssignments | ForEach-Object { ConvertTo-GuidValue -Value $_.id }) Write-TestInfo -Label 'Composition template selections' -Value @($composition.templateSelections).Count Write-TestInfo -Label 'Composition target assignments' -Value @($composition.targetAssignments).Count $deployments = @(ConvertTo-ApiItems -Value (Invoke-Api -Method GET -Path 'Deployment' -BearerToken $script:token) | Where-Object { [string]$_.deploymentGroupId -eq [string]$script:deploymentBatchId }) $deployments.Count | Should Be 3 Write-TestInfo -Label 'Deployment rows' -Value $deployments.Count Invoke-Api -Method GET -Path ("Deployment/{0}" -f $deployments[0].id) -BearerToken $script:token | Should Not BeNullOrEmpty Invoke-Api -Method GET -Path ("deployment-artifacts?deploymentGroupId={0}" -f $script:deploymentBatchId) -BearerToken $script:token | Out-Null Invoke-Api -Method GET -Path 'Deployment/QueueJobs' -BearerToken $script:token | Out-Null Write-TestInfo -Label 'Artifact and queue endpoints' -Value 'reachable' } } finally { if ($script:token) { Write-TestSection -Name 'cleanup' if ($script:credentialSecretId) { Invoke-Api -Method DELETE -Path ("credential-secrets/{0}" -f $script:credentialSecretId) -BearerToken $script:token -AllowNotFound | Out-Null Write-TestInfo -Label 'Deleted credential secret' -Value $script:credentialSecretId } if ($script:configurationValueId) { Invoke-Api -Method DELETE -Path ("configuration-values/{0}" -f $script:configurationValueId) -BearerToken $script:token -AllowNotFound | Out-Null Write-TestInfo -Label 'Deleted configuration value' -Value $script:configurationValueId } foreach ($assignmentId in @($script:targetAssignmentIds)) { if ($script:deploymentBatchId) { Invoke-Api -Method DELETE -Path ("deployment-batches/{0}/target-assignments/{1}" -f $script:deploymentBatchId, $assignmentId) -BearerToken $script:token -AllowNotFound | Out-Null } } if ($script:deploymentBatchId) { Invoke-Api -Method DELETE -Path ("deployment-batches/{0}" -f $script:deploymentBatchId) -BearerToken $script:token -AllowNotFound | Out-Null Write-TestInfo -Label 'Deleted deployment batch' -Value $script:deploymentBatchId } foreach ($targetId in @($script:targetIds)) { Invoke-Api -Method DELETE -Path ("Target/{0}" -f $targetId) -BearerToken $script:token -AllowNotFound | Out-Null } if (@($script:targetIds).Count -gt 0) { Write-TestInfo -Label 'Deleted targets' -Value @($script:targetIds).Count } foreach ($domainId in @($script:domainIds)) { if ($script:environmentId) { Invoke-Api -Method DELETE -Path ("Domain/{0}/Environment/{1}" -f $domainId, $script:environmentId) -BearerToken $script:token -AllowNotFound | Out-Null } Invoke-Api -Method DELETE -Path ("Domain/{0}" -f $domainId) -BearerToken $script:token -AllowNotFound | Out-Null } if (@($script:domainIds).Count -gt 0) { Write-TestInfo -Label 'Deleted domains' -Value @($script:domainIds).Count } if ($script:environmentId) { Invoke-Api -Method DELETE -Path ("Environment/{0}" -f $script:environmentId) -BearerToken $script:token -AllowNotFound | Out-Null Write-TestInfo -Label 'Deleted environment' -Value $script:environmentId } } } }