param( [string]$ApiBaseUrl = 'http://localhost:5286/api', [string]$BgwRoot = ('F:\Kunden Auftr' + [char]0x00E4 + 'ge\BGW'), [string]$DeploymentBatchId = '80000000-0000-0000-0000-000000000101', [string]$MergeModulePath = 'F:\Projekte\Coding\PowerShell\Merge-DSCConfigurationData\Merge-DSCConfigurationData.psd1', [string]$ResolveModulePath = 'F:\Projekte\Coding\PowerShell\Resolve-DSCConfigurationData\Resolve-DSCConfigurationData.psd1', [int]$ApiTimeoutSec = 30, [bool]$UseDefaultCredentials = $true ) $ErrorActionPreference = 'Stop' function Invoke-TestBgwMergeApiJson { param( [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] [string]$Method, [string]$Path ) $uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/')) $parameters = @{ Method = $Method Uri = $uri TimeoutSec = $ApiTimeoutSec ErrorAction = 'Stop' } if ($UseDefaultCredentials) { $parameters.UseDefaultCredentials = $true } try { Invoke-RestMethod @parameters } catch { $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 ConvertTo-TestBgwMergeConfigurationValue { param([object]$Value) if ($null -eq $Value) { return $null } if ($Value -is [string] -or $Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) { return $Value } if ($Value -is [System.Collections.IDictionary]) { $result = [ordered]@{} foreach ($key in $Value.Keys) { $result[[string]$key] = ConvertTo-TestBgwMergeConfigurationValue -Value $Value[$key] } Write-Output -NoEnumerate $result return } if ($Value -is [pscustomobject]) { $result = [ordered]@{} foreach ($property in $Value.PSObject.Properties) { $result[$property.Name] = ConvertTo-TestBgwMergeConfigurationValue -Value $property.Value } Write-Output -NoEnumerate $result return } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { $items = New-Object System.Collections.Generic.List[object] foreach ($item in $Value) { $items.Add((ConvertTo-TestBgwMergeConfigurationValue -Value $item)) } Write-Output -NoEnumerate $items.ToArray() return } return $Value } function ConvertFrom-TestBgwMergeTemplateJson { param([string]$JsonData) $document = $JsonData | ConvertFrom-Json $metadata = ConvertTo-TestBgwMergeConfigurationValue -Value $document.metadata if ($null -eq $metadata) { $metadata = [ordered]@{} } if (-not $metadata.Contains('TemplateType')) { $metadata['TemplateType'] = $document.templateType } [ordered]@{ Metadata = $metadata Parameters = ConvertTo-TestBgwMergeConfigurationValue -Value $document.parameters Variables = ConvertTo-TestBgwMergeConfigurationValue -Value $document.variables Resources = ConvertTo-TestBgwMergeConfigurationValue -Value $document.resources } } function ConvertFrom-TestBgwMergeTargetAssignments { param([object[]]$TargetAssignments) @($TargetAssignments | Sort-Object sortOrder | ForEach-Object { $nodeData = ConvertTo-TestBgwMergeConfigurationValue -Value ($_.nodeDataJson | ConvertFrom-Json) $node = @{} foreach ($key in $nodeData.Keys) { if ($key -eq 'nodeName') { $node.NodeName = $nodeData[$key] } else { $node[$key] = $nodeData[$key] } } $node }) } function ConvertTo-TestBgwMergeStableValue { param([object]$Value) if ($null -eq $Value) { return $null } if ($Value -is [string] -or $Value -is [bool] -or $Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) { return $Value } if ($Value -is [System.Collections.IDictionary]) { $result = [ordered]@{} foreach ($key in @($Value.Keys | Sort-Object)) { $result[[string]$key] = ConvertTo-TestBgwMergeStableValue -Value $Value[$key] } Write-Output -NoEnumerate $result return } if ($Value -is [pscustomobject]) { $result = [ordered]@{} foreach ($property in @($Value.PSObject.Properties | Sort-Object Name)) { $result[$property.Name] = ConvertTo-TestBgwMergeStableValue -Value $property.Value } Write-Output -NoEnumerate $result return } if ($Value -is [System.Collections.IEnumerable] -and $Value -isnot [string]) { $items = New-Object System.Collections.Generic.List[object] foreach ($item in $Value) { $items.Add((ConvertTo-TestBgwMergeStableValue -Value $item)) } Write-Output -NoEnumerate $items.ToArray() return } return $Value } function ConvertTo-TestBgwMergeStableJson { param([object]$Value) (ConvertTo-TestBgwMergeStableValue -Value $Value) | ConvertTo-Json -Depth 100 -Compress } function Invoke-TestBgwMergeSnapshotScript { param([string]$Script) $encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($Script)) $output = & powershell.exe -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encodedCommand if ($LASTEXITCODE -ne 0) { throw "Snapshot PowerShell process failed with exit code [$LASTEXITCODE]. Output: $($output -join [Environment]::NewLine)" } $json = ($output | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join [Environment]::NewLine if ([string]::IsNullOrWhiteSpace($json)) { throw 'Snapshot PowerShell process returned no JSON output.' } $json | ConvertFrom-Json } function New-TestBgwMergeSnapshotScript { param( [string]$ConfigurationDataPath, [switch]$BuildLocalBaseline ) $escapedBgwRoot = $BgwRoot.Replace("'", "''") $escapedMergeModulePath = $MergeModulePath.Replace("'", "''") $escapedResolveModulePath = $ResolveModulePath.Replace("'", "''") $escapedConfigurationDataPath = if ($ConfigurationDataPath) { $ConfigurationDataPath.Replace("'", "''") } else { '' } $buildLocalBaselineText = if ($BuildLocalBaseline) { 'True' } else { 'False' } @" `$ErrorActionPreference = 'Stop' `$ProgressPreference = 'SilentlyContinue' Import-Module '$escapedMergeModulePath' -Force Import-Module '$escapedResolveModulePath' -Force function ConvertTo-SnapshotStableValue { param([object]`$Value) if (`$null -eq `$Value) { return `$null } if (`$Value -is [string] -or `$Value -is [bool] -or `$Value -is [int] -or `$Value -is [long] -or `$Value -is [double] -or `$Value -is [decimal]) { return `$Value } if (`$Value -is [System.Collections.IDictionary]) { `$result = [ordered]@{} foreach (`$key in @(`$Value.Keys | Sort-Object)) { `$result[[string]`$key] = ConvertTo-SnapshotStableValue -Value `$Value[`$key] } Write-Output -NoEnumerate `$result return } if (`$Value -is [pscustomobject]) { `$result = [ordered]@{} foreach (`$property in @(`$Value.PSObject.Properties | Sort-Object Name)) { `$result[`$property.Name] = ConvertTo-SnapshotStableValue -Value `$property.Value } Write-Output -NoEnumerate `$result return } if (`$Value -is [System.Collections.IEnumerable] -and `$Value -isnot [string]) { `$items = New-Object System.Collections.Generic.List[object] foreach (`$item in `$Value) { `$items.Add((ConvertTo-SnapshotStableValue -Value `$item)) } Write-Output -NoEnumerate `$items.ToArray() return } return `$Value } function ConvertTo-SnapshotStableJson { param([object]`$Value) (ConvertTo-SnapshotStableValue -Value `$Value) | ConvertTo-Json -Depth 100 -Compress } if ('$buildLocalBaselineText' -eq 'True') { `$root = '$escapedBgwRoot' `$sourceTemplates = @( (Join-Path -Path `$root -ChildPath 'Environment\Test.psd1') (Join-Path -Path `$root -ChildPath 'Domain\Contoso.psd1') (Join-Path -Path `$root -ChildPath 'Service\SharePoint\Contoso.psd1') (Join-Path -Path `$root -ChildPath 'Stage\Install.psd1') ) `$allNodes = @( @{ NodeName = 'CLD-SHP-01'; RunCentralAdministration = `$true } @{ NodeName = 'CLD-SHP-02'; RunCentralAdministration = `$false } @{ NodeName = 'CLD-SHP-03'; RunCentralAdministration = `$false } ) `$configurationData = New-DSCConfigurationDataDeployment -Name 'Contoso-Test-SharePoint' -DeploymentId '8f6c2c1a' -SourceTemplatePath `$sourceTemplates -AllNodes `$allNodes } else { `$configurationData = Import-PowerShellDataFile -LiteralPath '$escapedConfigurationDataPath' } `$resolved = Resolve-DSCConfigurationData -ConfigurationData `$configurationData -SkipSecrets `$snapshot = [ordered]@{ ParametersJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Parameters VariablesJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Variables ResourcesJson = ConvertTo-SnapshotStableJson -Value `$configurationData.Resources DatabasePrefix = `$resolved.Variables.DatabasePrefix ServiceDbPrefix = `$resolved.Variables.ServiceDbPrefix ConfigDbName = `$resolved.Variables.ConfigDbName LcmConfigurationMode = `$resolved.Resources.NonNodeData.LocalConfigurationManager.ConfigurationMode AllNodes = @(`$resolved.Resources.AllNodes | Sort-Object NodeName | ForEach-Object { [ordered]@{ NodeName = `$_.NodeName RunCentralAdministration = `$_.RunCentralAdministration } }) } `$snapshot | ConvertTo-Json -Depth 100 -Compress "@ } function New-TestBgwMergeLocalSnapshot { Invoke-TestBgwMergeSnapshotScript -Script (New-TestBgwMergeSnapshotScript -BuildLocalBaseline) } function New-TestBgwMergeSnapshotFromConfigurationData { param([hashtable]$ConfigurationData) Import-Module $MergeModulePath -Force $tempPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ("BgwMergeApiSnapshot-{0}.psd1" -f ([guid]::NewGuid().ToString('N'))) try { Export-PowerShellDataFile -InputObject $ConfigurationData -Path $tempPath -Force Invoke-TestBgwMergeSnapshotScript -Script (New-TestBgwMergeSnapshotScript -ConfigurationDataPath $tempPath) } finally { if (Test-Path -LiteralPath $tempPath) { Remove-Item -LiteralPath $tempPath -Force } } } function New-TestBgwMergeApiConfigurationData { param([object]$Composition) Import-Module $MergeModulePath -Force $mergedTemplateData = @{} foreach ($selection in @($Composition.templateSelections | Sort-Object sortOrder)) { $templateData = ConvertFrom-TestBgwMergeTemplateJson -JsonData $selection.templateVersion.jsonData $mergedTemplateData = Merge-DSCConfigurationData -Template $mergedTemplateData -Deployment $templateData } $deploymentData = @{ Resources = @{ AllNodes = ConvertFrom-TestBgwMergeTargetAssignments -TargetAssignments @($Composition.targetAssignments) } } Merge-DSCConfigurationData -Template $mergedTemplateData -Deployment $deploymentData } Describe 'BGW Test.Merge.ps1 API DemoData composition' { BeforeAll { $script:expectedTemplateAliases = @( 'Environment-Test' 'Domain-Contoso' 'Service-SharePoint-Contoso' 'Stage-Install' ) $script:expectedTemplateRoles = @( 'Environment' 'Domain' 'Service' 'Stage' ) $script:localSnapshot = New-TestBgwMergeLocalSnapshot $script:apiComposition = Invoke-TestBgwMergeApiJson -Method GET -Path "deployment-batches/$DeploymentBatchId/composition" $script:apiConfigurationData = New-TestBgwMergeApiConfigurationData -Composition $script:apiComposition $script:apiSnapshot = New-TestBgwMergeSnapshotFromConfigurationData -ConfigurationData $script:apiConfigurationData } It 'loads the seeded deployment composition from the API' { $script:apiComposition.deploymentBatchId | Should Be $DeploymentBatchId @($script:apiComposition.templateSelections).Count | Should Be 4 @($script:apiComposition.targetAssignments).Count | Should Be 3 } It 'keeps the template selection order from Test.Merge.ps1' { $aliases = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.alias }) $roles = @($script:apiComposition.templateSelections | Sort-Object sortOrder | ForEach-Object { $_.templateRole }) ($aliases -join '|') | Should Be ($script:expectedTemplateAliases -join '|') ($roles -join '|') | Should Be ($script:expectedTemplateRoles -join '|') } It 'keeps the AllNodes data from Test.Merge.ps1' { $nodes = @($script:apiSnapshot.AllNodes | Sort-Object NodeName) $nodes.Count | Should Be 3 $nodes[0].NodeName | Should Be 'CLD-SHP-01' $nodes[0].RunCentralAdministration | Should Be $true $nodes[1].NodeName | Should Be 'CLD-SHP-02' $nodes[1].RunCentralAdministration | Should Be $false $nodes[2].NodeName | Should Be 'CLD-SHP-03' $nodes[2].RunCentralAdministration | Should Be $false } It 'resolves the same core values as the local Test.Merge.ps1 baseline' { $script:apiSnapshot.DatabasePrefix | Should Be $script:localSnapshot.DatabasePrefix $script:apiSnapshot.ServiceDbPrefix | Should Be $script:localSnapshot.ServiceDbPrefix $script:apiSnapshot.ConfigDbName | Should Be $script:localSnapshot.ConfigDbName $script:apiSnapshot.LcmConfigurationMode | Should Be $script:localSnapshot.LcmConfigurationMode } It 'matches merged Parameters, Variables and Resources' { $script:apiSnapshot.ParametersJson | Should Be $script:localSnapshot.ParametersJson $script:apiSnapshot.VariablesJson | Should Be $script:localSnapshot.VariablesJson $script:apiSnapshot.ResourcesJson | Should Be $script:localSnapshot.ResourcesJson } }