$script:ModuleRoot = Split-Path -Parent $PSScriptRoot $script:ModuleManifest = Join-Path -Path $script:ModuleRoot -ChildPath 'Merge-DSCConfigurationData.psd1' Import-Module $script:ModuleManifest -Force Describe 'Merge-DSCConfigurationData Extends' { It 'keeps the existing hashtable merge behavior' { $Template = @{ Parameters = @{ DomainFQDN = @{ Type = 'string' Required = $true DefaultValue = 'example.local' } } } $Deployment = @{ Parameters = @{ DomainFQDN = @{ Value = 'contoso.local' } } } $Result = Merge-DSCConfigurationData -Template $Template -Deployment $Deployment $Result.Parameters.DomainFQDN.Type | Should Be 'string' $Result.Parameters.DomainFQDN.Required | Should Be $true $Result.Parameters.DomainFQDN.Value | Should Be 'contoso.local' } It 'merges a child template with Metadata.Extends' { $DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1' $ChildPath = Join-Path -Path $TestDrive -ChildPath 'Contoso.psd1' @' @{ Metadata = @{ TemplateType = 'Environment' Name = 'Default' } Parameters = @{ DomainFQDN = @{ Type = 'string' Required = $true DefaultValue = 'example.local' } DomainNetBIOS = @{ Type = 'string' Required = $true } } Resources = @{ NonNodeData = @{ RequiredModules = @( @{ Name = 'ActiveDirectoryDsc' Version = '6.7.1' } ) } } } '@ | Set-Content -Path $DefaultPath -Encoding UTF8 @' @{ Metadata = @{ TemplateType = 'Environment' Name = 'Contoso' Extends = './Default.psd1' } Parameters = @{ DomainFQDN = @{ Value = 'contoso.local' } DomainNetBIOS = @{ Value = 'CONTOSO' } } } '@ | Set-Content -Path $ChildPath -Encoding UTF8 $Result = Merge-DSCConfigurationData -Path $ChildPath -PassThru $Result.Files.Count | Should Be 2 [System.IO.Path]::GetFileName($Result.Files[0]) | Should Be 'Default.psd1' [System.IO.Path]::GetFileName($Result.Files[1]) | Should Be 'Contoso.psd1' $Result.ConfigurationData.Parameters.DomainFQDN.Type | Should Be 'string' $Result.ConfigurationData.Parameters.DomainFQDN.Value | Should Be 'contoso.local' $Result.ConfigurationData.Parameters.DomainNetBIOS.Required | Should Be $true $Result.ConfigurationData.Parameters.DomainNetBIOS.Value | Should Be 'CONTOSO' $Result.ConfigurationData.Resources.NonNodeData.RequiredModules[0].Name | Should Be 'ActiveDirectoryDsc' $Result.ConfigurationData.ContainsKey('Metadata') | Should Be $false } It 'detects circular Metadata.Extends references' { $APath = Join-Path -Path $TestDrive -ChildPath 'A.psd1' $BPath = Join-Path -Path $TestDrive -ChildPath 'B.psd1' @' @{ Metadata = @{ Extends = './B.psd1' } } '@ | Set-Content -Path $APath -Encoding UTF8 @' @{ Metadata = @{ Extends = './A.psd1' } } '@ | Set-Content -Path $BPath -Encoding UTF8 { Merge-DSCConfigurationData -Path $APath } | Should Throw } It 'exports importable PSD1 files with quoted metadata keys' { $Path = Join-Path -Path $TestDrive -ChildPath 'Export.psd1' @{ Metadata = @{ Description = @{ 'de-DE' = 'Deutsch' 'en-US' = 'English' } } } | Export-PowerShellDataFile -Path $Path -Force $Data = Import-PowerShellDataFile -Path $Path $Data.Metadata.Description['de-DE'] | Should Be 'Deutsch' $Data.Metadata.Description['en-US'] | Should Be 'English' } It 'creates materialized deployment data without exporting by default' { $DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1' $ChildPath = Join-Path -Path $TestDrive -ChildPath 'Contoso.psd1' @' @{ Metadata = @{ TemplateType = 'Environment' Name = 'Default' } Parameters = @{ DomainFQDN = @{ Type = 'string' Required = $true } } Resources = @{ AllNodes = @( @{ NodeName = '*' PSDSCAllowPlainTextPassword = $true } ) } } '@ | Set-Content -Path $DefaultPath -Encoding UTF8 @' @{ Metadata = @{ TemplateType = 'Environment' Name = 'Contoso' Extends = './Default.psd1' } Parameters = @{ DomainFQDN = @{ Value = 'contoso.local' } } } '@ | Set-Content -Path $ChildPath -Encoding UTF8 $Result = New-DSCConfigurationDataDeployment ` -Name 'Contoso-Test' ` -DeploymentId '123' ` -SourceTemplatePath $ChildPath ` -AllNodes @( @{ NodeName = 'Node01' RunCentralAdministration = $true } ) $Result.Metadata.TemplateType | Should Be 'Deployment' $Result.Metadata.Sources.Files.Count | Should Be 2 $Result.Parameters.DomainFQDN.Value | Should Be 'contoso.local' $Result.Resources.AllNodes[0].NodeName | Should Be 'Node01' $Result.Resources.AllNodes[0].PSDSCAllowPlainTextPassword | Should Be $true } It 'exports materialized deployment data as PSD1' { $DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1' $ChildPath = Join-Path -Path $TestDrive -ChildPath 'Contoso.psd1' $DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployments\Deployment_123.psd1' @' @{ Metadata = @{ TemplateType = 'Environment' Name = 'Default' } Parameters = @{ DomainFQDN = @{ Type = 'string' Required = $true } } Variables = @{ } } '@ | Set-Content -Path $DefaultPath -Encoding UTF8 @' @{ Metadata = @{ TemplateType = 'Environment' Name = 'Contoso' Extends = './Default.psd1' } Parameters = @{ DomainFQDN = @{ Value = 'contoso.local' } } } '@ | Set-Content -Path $ChildPath -Encoding UTF8 $Result = New-DSCConfigurationDataDeployment ` -Path $DeploymentPath ` -Name 'Contoso-Test' ` -DeploymentId '123' ` -SourceTemplatePath $ChildPath ` -AllNodes @( @{ NodeName = 'Node01' RunCentralAdministration = $true } ) ` -UseRelativePaths ` -Export ` -Format PowerShellDataFile ` -Force ` -PassThru $DeploymentPath | Should Exist $Result.ConfigurationData.Metadata.TemplateType | Should Be 'Deployment' $Result.ConfigurationData.Metadata.Sources.Files.Count | Should Be 2 $Result.ConfigurationData.Metadata.Sources.Files[0].Hash.Length | Should Be 64 $Result.ConfigurationData.Metadata.Sources.Files[0].Algorithm | Should Be 'SHA256' $Result.ConfigurationData.Parameters.DomainFQDN.Value | Should Be 'contoso.local' $Result.ConfigurationData.Resources.AllNodes[0].NodeName | Should Be 'Node01' $Result.ConfigurationData.Resources.AllNodes[0].RunCentralAdministration | Should Be $true $Imported = Import-PowerShellDataFile -Path $DeploymentPath $Imported.Metadata.Name | Should Be 'Contoso-Test' $Imported.Metadata.Sources.Templates.Count | Should Be 1 $Validation = Test-DSCConfigurationDataDeployment -Path $DeploymentPath -PassThru $Validation.IsValid | Should Be $true $Validation.IsStale | Should Be $false } It 'exports materialized deployment data as JSON' { $DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1' $DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployment_123.json' @' @{ Parameters = @{ DomainFQDN = @{ Type = 'string' Value = 'contoso.local' } } Variables = @{ } } '@ | Set-Content -Path $DefaultPath -Encoding UTF8 New-DSCConfigurationDataDeployment ` -Path $DeploymentPath ` -Name 'Contoso-Test' ` -DeploymentId '123' ` -SourceTemplatePath $DefaultPath ` -Export ` -Format Json ` -Force $DeploymentPath | Should Exist $Imported = Get-Content -Path $DeploymentPath -Raw | ConvertFrom-Json $Imported.Metadata.TemplateType | Should Be 'Deployment' $Imported.Parameters.DomainFQDN.Value | Should Be 'contoso.local' } It 'detects changed deployment source files by hash' { $DefaultPath = Join-Path -Path $TestDrive -ChildPath 'Default.psd1' $DeploymentPath = Join-Path -Path $TestDrive -ChildPath 'Deployment_123.psd1' @' @{ Parameters = @{ DomainFQDN = @{ Type = 'string' Value = 'contoso.local' } } Variables = @{ } } '@ | Set-Content -Path $DefaultPath -Encoding UTF8 New-DSCConfigurationDataDeployment ` -Path $DeploymentPath ` -Name 'Contoso-Test' ` -DeploymentId '123' ` -SourceTemplatePath $DefaultPath ` -AllNodes @( @{ NodeName = 'Node01' } ) ` -Export ` -Force @' @{ Parameters = @{ DomainFQDN = @{ Type = 'string' Value = 'changed.local' } } Variables = @{ } } '@ | Set-Content -Path $DefaultPath -Encoding UTF8 $Validation = Test-DSCConfigurationDataDeployment -Path $DeploymentPath -PassThru $Validation.IsValid | Should Be $true $Validation.IsStale | Should Be $true @($Validation.Issues | Where-Object { $_.Code -eq 'SourceChanged' }).Count | Should Be 1 } It 'prevents overriding a sealed parameter definition' { $Template = @{ Parameters = @{ DomainNetBIOS = @{ Type = 'string' Value = 'CONTOSO' Sealed = $true } } } $Deployment = @{ Parameters = @{ DomainNetBIOS = @{ Value = 'OTHER' } } } { Merge-DSCConfigurationData -Template $Template -Deployment $Deployment } | Should Throw } It 'prevents adding values below a sealed resource tree' { $Template = @{ Resources = @{ NonNodeData = @{ Services = @{ SharePoint = @{ Farm = @{ ManagedAccounts = @{ Sealed = $true FarmAccount = "[parameters('FarmCredential')]" } } } } } } } $Deployment = @{ Resources = @{ NonNodeData = @{ Services = @{ SharePoint = @{ Farm = @{ ManagedAccounts = @{ SearchAccount = "[parameters('SearchCredential')]" } } } } } } } { Merge-DSCConfigurationData -Template $Template -Deployment $Deployment } | Should Throw } }