diff --git a/CHANGELOG.md b/CHANGELOG.md index e36c277..5808ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Changed - -- Remove `SkipReason` from RSOP test cases due to resolved merge logic bug. - -## [0.41.0] - 2026-02-03 - ### Added - Add configurable `default_json_depth` setting in `Datum.yml` to @@ -40,15 +34,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 RSOP.md. - Added AllNodes iteration examples for both flat and nested directory layouts in README.md, RSOP.md, and AboutDatum.md. +- Support conditional `ResolutionPrecedence` entries using + `Datum.InvokeCommand` expressions (`[x= ... =]`). Entries that + evaluate to `$null` or empty strings are now silently skipped + instead of causing lookup errors. +- Added knockout support for hashtable array items. +- Added integration tests for environment variable resolution in + `ResolutionPrecedence` paths + ([#126](https://github.com/gaelcolas/datum/pull/126)). ### Changed - Rewrote README.md with structured sections, table of contents, installation guide, merge strategy reference, handler documentation, and public function catalogue. +- Remove `SkipReason` from RSOP test cases due to resolved merge logic bug. +- Adjusted integration tests for knockout of hashtable array items. +- Adjusted integration tests for hashtable array merge behauvior 'Sum'. +- Add integration test for conditional `ResolutionPrecedence` entry + using an InvokeCommand expression that returns a path for some nodes + and nothing for others. ### Fixed +- Fixed `Join-Path` throwing `DriveNotFoundException` when + `ResolutionPrecedence` entries contain environment variable + references such as `$($env:VarName)`, by using + `[System.IO.Path]::Combine` instead + ([#126](https://github.com/gaelcolas/datum/pull/126)). - Fix `ConvertTo-Json` truncation warnings for deep data structures in `Merge-Datum`, `Merge-Hashtable`, and `Invoke-TestHandlerAction` @@ -61,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `$rsop.SomeKey.__source` pattern. - Documented `-IncludeSource`/`-RemoveSource` mutual exclusivity in RSOP.md and CmdletReference.md. +- Fixed merging of hashtable array items using merge behaviour 'Sum'. ## [0.41.0] - 2026-02-03 diff --git a/docs/CmdletReference.md b/docs/CmdletReference.md index 2df73af..e07b4ab 100644 --- a/docs/CmdletReference.md +++ b/docs/CmdletReference.md @@ -101,6 +101,11 @@ Resolve-Datum -PropertyPath 'Configurations' -Variable $Node -DatumTree $Datum | `PathPrefixes` | `[string[]]` | No | `$DatumTree.__Definition.ResolutionPrecedence` | Resolution paths. Alias: `SearchPaths` | | `MaxDepth` | `[int]` | No | From definition or `-1` | Maximum merge recursion depth | +> **Note:** `PathPrefixes` are processed through configured datum +> handlers before lookup begins. Entries that resolve to `$null` or +> empty/whitespace strings (e.g. conditional `[x= ... =]` expressions) +> are automatically removed from the search list. + **Output:** `[System.Array]` — The resolved value(s). --- diff --git a/docs/DatumYml.md b/docs/DatumYml.md index 11b60c3..18355e8 100644 --- a/docs/DatumYml.md +++ b/docs/DatumYml.md @@ -156,6 +156,40 @@ Any property of the node hashtable can be referenced. > **Tip:** `$Node.Name` is the file name of the node data file (set automatically by the FileProvider). `$Node.NodeName` is typically set inside the data file itself, often using `'[x={ $Node.Name }=]'` via InvokeCommand to derive it from the file name. +#### Conditional Entries with InvokeCommand + +Resolution paths are processed through configured datum handlers +**before** lookup begins. This means you can use +`Datum.InvokeCommand` expressions (`[x= ... =]`) to include a path +only when a condition is met: + +```yaml +ResolutionPrecedence: + - AllNodes\$($Node.Environment)\$($Node.NodeName) + - Environment\$($Node.Environment) + - Roles\$($Node.Role) + - Baselines\Security + - | + '[x= { + if ($Node.Name -notlike "*file*") + { + "Somevalue" + } + else + { + # returns $null — entry is skipped + } + } =]' +``` + +When the expression returns `$null` or an empty string, the entry +is silently removed from the search list for that node. This keeps +the lookup clean without requiring separate precedence lists per +node type. + +> **Note:** The multi-line YAML block-scalar syntax (`|`) is required +> so the `[x= ... =]` marker spans multiple lines correctly. + #### Path Format - Use **backslash** (`\`) as the path separator diff --git a/memory-bank/activeContext.md b/memory-bank/activeContext.md index a76ec16..383adef 100644 --- a/memory-bank/activeContext.md +++ b/memory-bank/activeContext.md @@ -1,28 +1,22 @@ # Active Context -## Current State (as of 2026-02-25) -The project is on the `main` branch. Recent work has focused on **PR #154 improvements** — making the `ConvertFrom-Yaml` consolidation for JSON files more comprehensive with proper CHANGELOG categorization and integration tests for JSON/YAML equivalence. +## Current State (as of 2026-03-07) +The project is on the `feature/docs` branch. Recent work has focused on adding support for **conditional ResolutionPrecedence entries** using `Datum.InvokeCommand` expressions. ## Current Work Focus The project has **unreleased changes** (tracked in CHANGELOG.md under [Unreleased]) that include: ### Unreleased Features & Fixes -1. **Knockout support for basetype arrays** — New feature allowing items to be removed from inherited arrays -2. **Cleanup of knockout items** — Post-merge cleanup of knockout markers -3. **Pester tests for credential handling** — New test coverage -4. **ConvertTo-Datum fix** — Fixed returning `` when DatumHandler returns `False` (#139) -5. **Merge-DatumArray fix** — Fixed not returning array when merged array contains single hashtable -6. **Hashtable array merge fix** — Fixed items not merging when using datum handler for tuple keys (#155) -7. **Copy-Object fixes** — Fixed and extended tests, PowerShell 7 compatibility -8. **Pester 5 migration** — All integration tests migrated to Pester 5 syntax -9. **Build system update** — Updated to Sampler 0.119.0-preview0005 -10. **Merge-DatumArray improvement** — Convert tuple key values to datum before merging +1. **Conditional ResolutionPrecedence entries** — `Resolve-Datum` now filters out null/empty/whitespace path prefixes after datum handler invocation, enabling conditional `[x= ... =]` expressions in `ResolutionPrecedence` that can return nothing for certain nodes. +2. **Integration test for conditional precedence** — Added InvokeCommand expression to test `Datum.yml` that conditionally includes a path only for non-file-server nodes. +3. **SkipReason removal** — Removed `SkipReason` from RSOP test cases due to resolved merge logic bug. ## Recent Changes (Last Session) -- Moved CHANGELOG entry for PR #154 from `Added` to `Changed` with expanded description and PR link -- Created `tests/Integration/GetFileProviderData.tests.ps1` — 33 new tests for JSON/YAML equivalence -- Created test data in `tests/Integration/assets/JsonYamlEquivalence/` (JSON + YAML pairs) -- **Build result**: 191 passed, 0 failed, 3 skipped (pre-existing) +- Added `Where-Object { -not [string]::IsNullOrWhiteSpace($_) }` filter in `Resolve-Datum.ps1` after `ConvertTo-Datum` handler processing of `$PathPrefixes` +- Added conditional InvokeCommand expression to `tests/Integration/assets/DscWorkshopConfigData/Datum.yml` +- Updated CHANGELOG.md with Added and Changed entries under [Unreleased] +- Added "Conditional Entries with InvokeCommand" section to `docs/DatumYml.md` +- Added PathPrefixes filtering note to `docs/CmdletReference.md` ## Next Steps - ~~**Fix Issue #136**~~: DONE — depth now configurable via `default_json_depth` in Datum.yml (default 4), 8 tests, zero truncation warnings. Build verified, all tests passing. Ready for PR. diff --git a/memory-bank/techContext.md b/memory-bank/techContext.md index 510eb02..e56d3bf 100644 --- a/memory-bank/techContext.md +++ b/memory-bank/techContext.md @@ -10,6 +10,22 @@ - **Versioning**: GitVersion (Semantic Versioning) - **Package Distribution**: PowerShell Gallery +## Running Tests +**CRITICAL**: Never use the `runTests` tool or run Pester inside VS Code's integrated PowerShell session — it will hang VS Code. Always run tests in a **separate `pwsh` process**: + +```powershell +# Full build + test (Sampler) +pwsh -NoProfile -NonInteractive -Command "./build.ps1 -Tasks test" + +# Single test file +pwsh -NoProfile -NonInteractive -Command "Invoke-Pester -Path './tests/Integration/Resolve-Datum.Tests.ps1' -Output Detailed" + +# With module pre-loaded (for integration tests that need the built module) +pwsh -NoProfile -NonInteractive -Command "Import-Module ./output/datum/0.0.1/datum.psd1 -Force; Invoke-Pester -Path './tests/Integration/' -Output Detailed" +``` + +Use `run_in_terminal` with `isBackground = false` and a generous timeout (180000+ ms). + ## Dependencies ### Runtime - `powershell-yaml` — YAML parsing (required module) diff --git a/source/Private/Clear-DatumKnockout.ps1 b/source/Private/Clear-DatumKnockout.ps1 index 6b50850..9855c5e 100644 --- a/source/Private/Clear-DatumKnockout.ps1 +++ b/source/Private/Clear-DatumKnockout.ps1 @@ -117,28 +117,20 @@ function Clear-DatumKnockout { if ($strategy.merge_hash_array -match '^MostSpecific$|^First') { - return $ReferenceDatum + return , $ReferenceDatum } if ($knockoutPrefixMatcher = $strategy.merge_options.knockout_prefix) { - if ($tupleKeyNames = [string[]]$Strategy.merge_options.tuple_keys) + $knockoutPrefixMatcher = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^') + + if ($tupleKeyNames = [string[]]$strategy.merge_options.tuple_keys) { - $tupleKeyNames += $tupleKeyNames.ForEach{ $strategy.merge_options.knockout_prefix + $_ } $knockoutItems = foreach ($refItem in $ReferenceDatum) { - $refItemTupleKeys = $refItem.Keys.Where{ $_ -in $tupleKeyNames } - if ($refItemTupleKeys -match $knockoutPrefixMatcher) + if ($refItem.Keys.Where{ $_ -in $tupleKeyNames -and $refItem[$_] -match $knockoutPrefixMatcher }) { $refItem - $filterScript = @() - foreach ($refItemTupleKey in $refItemTupleKeys) - { - $refItemTupleKeyWithoutKnockoutPrefix = $refItemTupleKey -replace $knockoutPrefixMatcher, '' - $filterScript += "`$_.$refItemTupleKeyWithoutKnockoutPrefix -eq '$($refItem."$refItemTupleKey")'" - } - $filterScript = [scriptblock]::Create($filterScript -join ' -and ') - $ReferenceDatum.Where{ &$filterScript } } } foreach ($knockoutItem in $knockoutItems) @@ -148,19 +140,19 @@ function Clear-DatumKnockout } } - $cleanedArray = [System.Collections.ArrayList]::new() - - foreach ($currentItem in $ReferenceDatum) - { - $cleanupItemParams = @{ - StartingPath = $StartingPath - ReferenceDatum = $currentItem - Strategies = $Strategies + $cleanedArray = @( + foreach ($currentItem in $ReferenceDatum) + { + $cleanupItemParams = @{ + StartingPath = $StartingPath + ReferenceDatum = $currentItem + Strategies = $Strategies + } + Clear-DatumKnockout @cleanupItemParams } - $null = $cleanedArray.Add((Clear-DatumKnockout @cleanupItemParams)) - } + ) - return (, $cleanedArray) + return , $cleanedArray } default diff --git a/source/Private/Merge-DatumArray.ps1 b/source/Private/Merge-DatumArray.ps1 index 6668809..e5cfc0c 100644 --- a/source/Private/Merge-DatumArray.ps1 +++ b/source/Private/Merge-DatumArray.ps1 @@ -27,7 +27,6 @@ function Merge-DatumArray ) Write-Debug -Message "`tMerge-DatumArray -StartingPath <$StartingPath>" - $knockout_prefix = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^') $hashArrayStrategy = $Strategy.merge_hash_array Write-Debug -Message "`t`tHash Array Strategy: $hashArrayStrategy" $mergeBasetypeArraysStrategy = $Strategy.merge_basetype_array @@ -53,12 +52,52 @@ function Merge-DatumArray return $mergedArray } + $knockedOutTupleKeyValues = [System.Collections.ArrayList]@() + foreach ($referenceItem in $ReferenceArray) + { + $currentRefItem = [ordered]@{} + $referenceItem + + # make sure property values are converted before merge + $result = $null + foreach ($prop in $propertyNames.Where{ $currentRefItem.Contains($_) }) + { + if (Invoke-DatumHandler -InputObject $currentRefItem[$prop] -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result)) + { + $currentRefItem[$prop] = ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers + } + } + + if ($knockoutPrefixMatcher = $Strategy.merge_options.knockout_prefix) + { + $knockoutPrefixMatcher = [regex]::Escape($Strategy.merge_options.knockout_prefix).Insert(0, '^') + + if ($tupleKeyNames = [string[]]$strategy.merge_options.tuple_keys) + { + if ($currentRefItemKeysWithKnockOutValues = $currentRefItem.Keys.Where{ $_ -in $tupleKeyNames -and $currentRefItem[$_] -match $knockoutPrefixMatcher }) + { + $ht = @{} + foreach ($key in $currentRefItemKeysWithKnockOutValues) + { + $ht.$key = $currentRefItem.$key -replace $knockoutPrefixMatcher + } + + $null = $knockedOutTupleKeyValues.Add($ht) + } + } + } + } + switch -Regex ($hashArrayStrategy) { '^Sum|^Add' { - (@($DifferenceArray) + @($ReferenceArray)) | ForEach-Object { - $null = $mergedArray.Add(([ordered]@{} + $_)) + foreach ($referenceItem in $ReferenceArray) + { + $null = $mergedArray.Add(([ordered]@{} + $referenceItem)) + } + foreach ($differenceItem in $DifferenceArray) + { + $null = $mergedArray.Add(([ordered]@{} + $differenceItem)) } } @@ -74,7 +113,7 @@ function Merge-DatumArray # if not found, add $DiffItem to $RefArray # look at each $RefItems in $RefArray - $usedDiffItems = [System.Collections.ArrayList]::new() + $usedOrKnockedOutDiffItems = [System.Collections.ArrayList]@() foreach ($referenceItem in $ReferenceArray) { $referenceItem = [ordered]@{} + $referenceItem @@ -85,18 +124,6 @@ function Merge-DatumArray Write-Debug -Message "`t`t`t ..No PropertyName defined: Use ReferenceItem Keys" $propertyNames = $referenceItem.Keys } - # make sure property values are converted before merge - $result = $null - foreach ($prop in $propertyNames) - { - if ($referenceItem.Contains($prop)) - { - if (Invoke-DatumHandler -InputObject $referenceItem.$prop -DatumHandlers $Datum.__Definition.DatumHandlers -Result ([ref]$result)) - { - $referenceItem.$prop = ConvertTo-Datum -InputObject $result -DatumHandlers $Datum.__Definition.DatumHandlers - } - } - } $mergedItem = @{} + $referenceItem $diffItemsToMerge = $DifferenceArray.Where{ $differenceItem = [ordered]@{} + $_ @@ -112,13 +139,31 @@ function Merge-DatumArray } } } - # Search for DiffItem that has the same Property/Value pairs than RefItem - $compareHashParams = @{ - ReferenceHashtable = [ordered]@{} + $referenceItem - DifferenceHashtable = $differenceItem - Property = $propertyNames + $itemKnockedOut = $false + foreach ($knockedOutTupleKeyValue in $knockedOutTupleKeyValues) + { + $filterStrings = foreach ($knockedOutTupleKeyValueKey in $knockedOutTupleKeyValue.Keys) + { + "`$knockedOutTupleKeyValue.'$knockedOutTupleKeyValueKey' -eq `$differenceItem.'$knockedOutTupleKeyValueKey'" + } + $filterScript = [scriptblock]::Create($filterStrings -join ' -and ') + if ( &$filterScript ) + { + $null = $usedOrKnockedOutDiffItems.Add($_) + $itemKnockedOut = $true + break + } + } + if ($itemKnockedOut -eq $false) + { + # Search for DiffItem that has the same Property/Value pairs than RefItem + $compareHashParams = @{ + ReferenceHashtable = [ordered]@{} + $referenceItem + DifferenceHashtable = $differenceItem + Property = $propertyNames + } + (-not (Compare-Hashtable @compareHashParams)) } - (-not (Compare-Hashtable @compareHashParams)) } Write-Debug -Message "`t`t`t ..Items to merge: $($diffItemsToMerge.Count)" $diffItemsToMerge | ForEach-Object { @@ -132,11 +177,11 @@ function Merge-DatumArray $mergedItem = Merge-Hashtable @mergeItemsParams } # If a diff Item has been used, save it to find the unused ones - $null = $usedDiffItems.AddRange($diffItemsToMerge) + $null = $usedOrKnockedOutDiffItems.AddRange($diffItemsToMerge) $null = $mergedArray.Add($mergedItem) } $unMergedItems = $DifferenceArray | ForEach-Object { - if (-not $usedDiffItems.Contains($_)) + if (-not $usedOrKnockedOutDiffItems.Contains($_)) { ([ordered]@{} + $_) } @@ -173,7 +218,7 @@ function Merge-DatumArray $mergedArray = [System.Collections.ArrayList]::new() $ReferenceArray | ForEach-Object { - $currentRefItem = $_ + $currentRefItem = [ordered]@{} + $_ # make sure property values are converted before merge $result = $null foreach ($prop in $propertyNames) @@ -193,7 +238,7 @@ function Merge-DatumArray } $DifferenceArray | ForEach-Object { - $currentDiffItem = $_ + $currentDiffItem = [ordered]@{} + $_ # make sure property values are converted before merge $result = $null foreach ($prop in $propertyNames) @@ -206,9 +251,26 @@ function Merge-DatumArray } } } - if (-not ($mergedArray.Where{ -not (Compare-Hashtable -Property $propertyNames -ReferenceHashtable $currentDiffItem -DifferenceHashtable $_ ) })) + $itemKnockedOut = $false + foreach ($knockedOutTupleKeyValue in $knockedOutTupleKeyValues) { - $null = $mergedArray.Add(([ordered]@{} + $_)) + $filterStrings = foreach ($knockedOutTupleKeyValueKey in $knockedOutTupleKeyValue.Keys) + { + "`$knockedOutTupleKeyValue.'$knockedOutTupleKeyValueKey' -eq `$currentDiffItem.'$knockedOutTupleKeyValueKey'" + } + $filterScript = [scriptblock]::Create($filterStrings -join ' -and ') + if ( &$filterScript ) + { + $itemKnockedOut = $true + break + } + } + if ($itemKnockedOut -eq $false) + { + if (-not ($mergedArray.Where{ -not (Compare-Hashtable -Property $propertyNames -ReferenceHashtable $currentDiffItem -DifferenceHashtable $_ ) })) + { + $null = $mergedArray.Add(([ordered]@{} + $_)) + } } } } diff --git a/source/Public/Merge-Datum.ps1 b/source/Public/Merge-Datum.ps1 index a06501d..f74eb12 100644 --- a/source/Public/Merge-Datum.ps1 +++ b/source/Public/Merge-Datum.ps1 @@ -190,10 +190,7 @@ function Merge-Datum '^Sum' { #--> $ref + $diff - (@($DifferenceArray) + @($ReferenceArray)).Foreach{ - $null = $MergedArray.Add(([ordered]@{} + $_)) - } - return (, $MergedArray) + return (, (Merge-DatumArray @MergeDatumArrayParams)) } Default diff --git a/source/Public/Resolve-Datum.ps1 b/source/Public/Resolve-Datum.ps1 index 23890b6..8188883 100644 --- a/source/Public/Resolve-Datum.ps1 +++ b/source/Public/Resolve-Datum.ps1 @@ -173,6 +173,7 @@ function Resolve-Datum #Invoke datum handlers $PathPrefixes = $PathPrefixes | ConvertTo-Datum -DatumHandlers $datum.__Definition.DatumHandlers + $PathPrefixes = $PathPrefixes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } $allMergeResults = [System.Collections.Stack]::new() $Script:previousMergeResult = $null diff --git a/tests/Integration/EnvVarResolution.tests.ps1 b/tests/Integration/EnvVarResolution.tests.ps1 new file mode 100644 index 0000000..f647b74 --- /dev/null +++ b/tests/Integration/EnvVarResolution.tests.ps1 @@ -0,0 +1,56 @@ +using module datum + +Remove-Module -Name datum + +Describe 'Environment variable resolution in ResolutionPrecedence paths' { + BeforeAll { + $here = $PSScriptRoot + Import-Module -Name datum -Force + + $env:DatumTestBaseline = 'TestBaseline' + + $datumPath = Join-Path -Path $here -ChildPath 'assets\EnvVarResolutionTestData\Datum.yml' + if (-not (Test-Path $datumPath)) + { + throw "Cannot find Datum.yml at: $datumPath (here = $here)" + } + + $datum = New-DatumStructure -DefinitionFile $datumPath + $allNodes = @($datum.AllNodes.psobject.Properties | ForEach-Object { + $node = $datum.AllNodes.($_.Name) + (@{} + $node) + }) + } + + AfterAll { + Remove-Item -Path Env:\DatumTestBaseline -ErrorAction SilentlyContinue + } + + Context 'ResolutionPrecedence entries with environment variables' { + + It 'Should resolve a property from a baseline referenced via an environment variable' { + $myNode = $allNodes.Where({ $_.NodeName -eq 'TestNode01' }) + $result = Resolve-Datum -PropertyPath 'TestSetting' -Variable $myNode -DatumTree $datum + $result | Should -Be 'BaselineValue' + } + + It 'Should not throw a DriveNotFoundException when using $env: in ResolutionPrecedence' { + $myNode = $allNodes.Where({ $_.NodeName -eq 'TestNode01' }) + { Resolve-Datum -PropertyPath 'TestSetting' -Variable $myNode -DatumTree $datum } | Should -Not -Throw + } + + It 'Should resolve a property from a mid-path environment variable reference' { + $env:DatumTestRole = 'WebServer' + try + { + $myNode = $allNodes.Where({ $_.NodeName -eq 'TestNode01' }) + $result = Resolve-Datum -PropertyPath 'RoleSetting' -Variable $myNode -DatumTree $datum + $result | Should -Be 'WebServerConfigValue' + } + finally + { + Remove-Item -Path Env:\DatumTestRole -ErrorAction SilentlyContinue + } + } + } +} diff --git a/tests/Integration/Merge.tests.ps1 b/tests/Integration/Merge.tests.ps1 index 5e3c18f..5b16d1e 100644 --- a/tests/Integration/Merge.tests.ps1 +++ b/tests/Integration/Merge.tests.ps1 @@ -53,21 +53,20 @@ Describe 'Merge ' { PropertyPath = 'WindowsFeatures\Name' Count = 4 } - @{ Node = 'DSCFile01' PropertyPath = 'Configurations' - Count = 3 + Count = 7 } @{ Node = 'DSCWeb01' PropertyPath = 'Configurations' - Count = 2 + Count = 6 } @{ Node = 'DSCWeb02' PropertyPath = 'Configurations' - Count = 3 + Count = 7 } ) @@ -82,17 +81,17 @@ Describe 'Merge ' { @{ Node = 'DSCFile01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures', 'FilesAndFolders' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures' + Value = 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb02' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'FilesAndFolders', 'WindowsFeatures' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCFile01' diff --git a/tests/Integration/Rsop.tests.ps1 b/tests/Integration/Rsop.tests.ps1 index 038dd82..d7a8b58 100644 --- a/tests/Integration/Rsop.tests.ps1 +++ b/tests/Integration/Rsop.tests.ps1 @@ -40,17 +40,17 @@ Describe "RSOP tests based on 'MergeTestData' test data" { @{ Node = 'DSCFile01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures', 'FilesAndFolders' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb01' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'WindowsFeatures' + Value = 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCWeb02' PropertyPath = 'Configurations' - Value = 'NetworkIpConfigurationMerged', 'FilesAndFolders', 'WindowsFeatures' + Value = 'FilesAndFolders', 'LocalUsers', 'NetworkIpConfigurationMerged', 'RegistryValues', 'SecurityOptions', 'SummaryConfig', 'WindowsFeatures' } @{ Node = 'DSCFile01' @@ -147,6 +147,78 @@ Describe "RSOP tests based on 'MergeTestData' test data" { PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Count' Value = 4 } + # DSCFile01 - LocalUsers - LocalAdmin + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "LocalAdmin"}.Ensure' + Value = 'Present' + } + # DSCFile01 - LocalUsers - Admin1 + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "Admin1"}.Ensure' + Value = $null + } + # DSCFile01 - LocalUsers count + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.Count' + Value = 1 + } + # DSCFile01 - LocalUsers UserName's + @{ + Node = 'DSCFile01' + PropertyPath = 'LocalUsers.Users.UserName' + Value = 'LocalAdmin' + } + # DSCFile01 - RegistryValues - DevicesPickerUserSvc - Start + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "Start"}.Ensure' + Value = $null + } + # DSCFile01 - RegistryValues - DevicesPickerUserSvc - UserServiceFlags + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "UserServiceFlags"}.Ensure' + Value = 'Present' + } + # DSCFile01 - RegistryValues count + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Count' + Value = 3 + } + # DSCFile01 - RegistryValues UserName's + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.Key' + Value = 'HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService' + } + # DSCFile01 - RegistryValues UserName's + @{ + Node = 'DSCFile01' + PropertyPath = 'RegistryValues.Values.ValueName' + Value = 'UserServiceFlags', 'Start', 'UserServiceFlags' + } + # DSCFile01 - Accounts_Rename_administrator_account + @{ + Node = 'DSCFile01' + PropertyPath = 'SecurityOptions.Policies.Where{$_.Name -eq "Accounts_Rename_administrator_account"}.Accounts_Rename_administrator_account' + Value = 'LocalAdmin' + } + # DSCFile01 - SummaryConfig count + @{ + Node = 'DSCFile01' + PropertyPath = 'SummaryConfig.SumItems.Count' + Value = 3 + } + # DSCFile01 - SummaryConfig ItemNumbers + @{ + Node = 'DSCFile01' + PropertyPath = 'SummaryConfig.SumItems.ItemNumber' + Value = 1, 2, 3 + } # DSCWeb01 - Ethernet 1 @{ Node = 'DSCWeb01' @@ -158,6 +230,90 @@ Describe "RSOP tests based on 'MergeTestData' test data" { PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 1"}.Gateway' Value = $null } + # DSCWeb01 - Interface count + @{ + Node = 'DSCWeb01' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Count' + Value = 2 + } + # DSCWeb01 - Ethernet 2 + @{ + Node = 'DSCWeb01' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 2"}' + Value = $null + } + # DSCWeb01 - LocalUsers - LocalAdmin + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "LocalAdmin"}.Ensure' + Value = $null + } + # DSCWeb01 - LocalUsers - Admin1 + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "Admin1"}.Ensure' + Value = $null + } + # DSCWeb01 - LocalUsers count + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.Count' + Value = 0 + } + # DSCWeb01 - LocalUsers UserName's + @{ + Node = 'DSCWeb01' + PropertyPath = 'LocalUsers.Users.UserName' + Value = $null + } + # DSCWeb01 - RegistryValues - DevicesPickerUserSvc - Start + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "Start"}.Ensure' + Value = $null + } + # DSCWeb01 - RegistryValues - DevicesPickerUserSvc - UserServiceFlags + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "UserServiceFlags"}.Ensure' + Value = $null + } + # DSCWeb01 - RegistryValues count + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Count' + Value = 2 + } + # DSCWeb01 - RegistryValues UserName's + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.Key' + Value = 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService' + } + # DSCWeb01 - RegistryValues UserName's + @{ + Node = 'DSCWeb01' + PropertyPath = 'RegistryValues.Values.ValueName' + Value = 'Start', 'UserServiceFlags' + } + # DSCWeb01 - Accounts_Rename_administrator_account + @{ + Node = 'DSCWeb01' + PropertyPath = 'SecurityOptions.Policies.Where{$_.Name -eq "Accounts_Rename_administrator_account"}.Accounts_Rename_administrator_account' + Value = $null + } + # DSCWeb01 - SummaryConfig count + @{ + Node = 'DSCWeb01' + PropertyPath = 'SummaryConfig.SumItems.Count' + Value = 3 + } + # DSCWeb01 - SummaryConfig ItemNumbers + @{ + Node = 'DSCWeb01' + PropertyPath = 'SummaryConfig.SumItems.ItemNumber' + Value = 1, 2, 4 + } # DSCWeb02 - Ethernet 1 @{ Node = 'DSCWeb02' @@ -169,6 +325,90 @@ Describe "RSOP tests based on 'MergeTestData' test data" { PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 1"}.Gateway' Value = '192.168.10.50' } + # DSCWeb02 - Interface count + @{ + Node = 'DSCWeb02' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Count' + Value = 2 + } + # DSCWeb02 - Ethernet 2 + @{ + Node = 'DSCWeb02' + PropertyPath = 'NetworkIpConfigurationMerged.Interfaces.Where{$_.InterfaceAlias -eq "Ethernet 2"}' + Value = $null + } + # DSCWeb02 - LocalUsers - LocalAdmin + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "LocalAdmin"}.Ensure' + Value = 'Absent' + } + # DSCWeb02 - LocalUsers - Admin1 + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.Where{$_.UserName -eq "Admin1"}.Ensure' + Value = 'Present' + } + # DSCWeb02 - LocalUsers count + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.Count' + Value = 2 + } + # DSCWeb02 - LocalUsers UserName's + @{ + Node = 'DSCWeb02' + PropertyPath = 'LocalUsers.Users.UserName' + Value = 'LocalAdmin', 'Admin1' + } + # DSCWeb02 - RegistryValues - DevicesPickerUserSvc - Start + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "Start"}.Ensure' + Value = 'Absent' + } + # DSCWeb02 - RegistryValues - DevicesPickerUserSvc - UserServiceFlags + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Where{$_.Key -eq "HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc" -and $_.ValueName -eq "UserServiceFlags"}.Ensure' + Value = $null + } + # DSCWeb02 - RegistryValues count + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Count' + Value = 3 + } + # DSCWeb02 - RegistryValues UserName's + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.Key' + Value = 'HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService', 'HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService' + } + # DSCWeb02 - RegistryValues UserName's + @{ + Node = 'DSCWeb02' + PropertyPath = 'RegistryValues.Values.ValueName' + Value = 'Start', 'Start', 'UserServiceFlags' + } + # DSCWeb02 - Accounts_Rename_administrator_account + @{ + Node = 'DSCWeb02' + PropertyPath = 'SecurityOptions.Policies.Where{$_.Name -eq "Accounts_Rename_administrator_account"}.Accounts_Rename_administrator_account' + Value = 'Admin1' + } + # DSCWeb02 - SummaryConfig count + @{ + Node = 'DSCWeb02' + PropertyPath = 'SummaryConfig.SumItems.Count' + Value = 2 + } + # DSCWeb02 - SummaryConfig ItemNumbers + @{ + Node = 'DSCWeb02' + PropertyPath = 'SummaryConfig.SumItems.ItemNumber' + Value = 1, 2 + } ) It "The value of Datum RSOP property '' for node '' should be ''." -ForEach $script:testCases { diff --git a/tests/Integration/assets/DscWorkshopConfigData/Datum.yml b/tests/Integration/assets/DscWorkshopConfigData/Datum.yml index a3c3f61..de21617 100644 --- a/tests/Integration/assets/DscWorkshopConfigData/Datum.yml +++ b/tests/Integration/assets/DscWorkshopConfigData/Datum.yml @@ -6,6 +6,17 @@ ResolutionPrecedence: - Baselines\Security - Baselines\$($Node.Baseline) - Baselines\DscLcm + - | + '[x= { + if ($node.Name -notlike "*file*") + { + 'Somevalue' + } + else + { + #nothing + } + } =]' DatumHandlersThrowOnError: false DatumHandlers: diff --git a/tests/Integration/assets/EnvVarResolutionTestData/AllNodes/TestNode01.yml b/tests/Integration/assets/EnvVarResolutionTestData/AllNodes/TestNode01.yml new file mode 100644 index 0000000..2c9a999 --- /dev/null +++ b/tests/Integration/assets/EnvVarResolutionTestData/AllNodes/TestNode01.yml @@ -0,0 +1,3 @@ +NodeName: TestNode01 +Description: Test node for environment variable resolution +Role: WebServer diff --git a/tests/Integration/assets/EnvVarResolutionTestData/Baselines/TestBaseline.yml b/tests/Integration/assets/EnvVarResolutionTestData/Baselines/TestBaseline.yml new file mode 100644 index 0000000..140104e --- /dev/null +++ b/tests/Integration/assets/EnvVarResolutionTestData/Baselines/TestBaseline.yml @@ -0,0 +1 @@ +TestSetting: BaselineValue diff --git a/tests/Integration/assets/EnvVarResolutionTestData/Datum.yml b/tests/Integration/assets/EnvVarResolutionTestData/Datum.yml new file mode 100644 index 0000000..e2f44f8 --- /dev/null +++ b/tests/Integration/assets/EnvVarResolutionTestData/Datum.yml @@ -0,0 +1,6 @@ +ResolutionPrecedence: + - AllNodes\$($Node.NodeName) + - Roles\$($env:DatumTestRole)\Config + - Baselines\$($env:DatumTestBaseline) + +default_lookup_options: MostSpecific diff --git a/tests/Integration/assets/EnvVarResolutionTestData/Roles/WebServer/Config.yml b/tests/Integration/assets/EnvVarResolutionTestData/Roles/WebServer/Config.yml new file mode 100644 index 0000000..18b2856 --- /dev/null +++ b/tests/Integration/assets/EnvVarResolutionTestData/Roles/WebServer/Config.yml @@ -0,0 +1 @@ +RoleSetting: WebServerConfigValue diff --git a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml index dc6a7e1..0046e1c 100644 --- a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml +++ b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb01.yml @@ -44,3 +44,9 @@ LcmConfig: ConfigurationRepositoryWeb: Server: ConfigurationNames: DSCWeb01 + +SummaryConfig: + SumItems: + - ItemName: Item1 + ItemNumber: 4 + Decription: Item1 with number 4 diff --git a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml index 415a375..c1786cb 100644 --- a/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml +++ b/tests/Integration/assets/MergeTestData/AllNodes/DSCWeb02.yml @@ -12,6 +12,18 @@ WindowsFeatures: Configurations: - FilesAndFolders +LocalUsers: + Users: + - UserName: Admin1 + Ensure: Present + Disabled: false + PasswordChangeRequired: false + PasswordNeverExpires: false + - UserName: LocalAdmin + Ensure: Absent + DependsOn: + - '[SecurityOption]secPolOpt_Accounts_Rename_administrator_account::[SecurityOptions]SecurityOptions' + NetworkIpConfigurationMerged: Interfaces: - InterfaceAlias: Ethernet 1 @@ -22,6 +34,19 @@ NetworkIpConfigurationNonMerged: - InterfaceAlias: Ethernet 1 IpAddress: 192.168.10.102 +RegistryValues: + Values: + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: Start + Ensure: Absent + Force: true + +SecurityOptions: + Policies: + - Name: Accounts_Rename_administrator_account + Accounts_Rename_administrator_account: Admin1 + + PSDscAllowPlainTextPassword: True PSDscAllowDomainUser: True diff --git a/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml b/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml index 93b5cb9..0dfec02 100644 --- a/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml +++ b/tests/Integration/assets/MergeTestData/Baselines/ServerBaseline.yml @@ -1,6 +1,10 @@ Configurations: - WindowsFeatures - NetworkIpConfigurationMerged + - LocalUsers + - RegistryValues + - SecurityOptions + - SummaryConfig WindowsFeatures: Name: @@ -39,3 +43,42 @@ NetworkIpConfigurationMerged: SecurityBase: Role: Baseline + +LocalUsers: + Users: + - UserName: LocalAdmin + Ensure: Present + Disabled: false + PasswordChangeRequired: false + PasswordNeverExpires: false + DependsOn: + - '[SecurityOption]secPolOpt_Accounts_Rename_administrator_account::[SecurityOptions]SecurityOptions' + +RegistryValues: + Values: + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: UserServiceFlags + ValueData: 0 + ValueType: Dword + Ensure: Present + Force: true + +SecurityOptions: + Policies: + - Name: Accounts_Rename_administrator_account + Accounts_Rename_administrator_account: LocalAdmin + +SummaryConfig: + SumItems: + - ItemName: Item1 + ItemNumber: 1 + Decription: Item1 with number 1 + - ItemName: Item1 + ItemNumber: 2 + Decription: Item1 with number 2 diff --git a/tests/Integration/assets/MergeTestData/Datum.yml b/tests/Integration/assets/MergeTestData/Datum.yml index 510b8d2..272a4b5 100644 --- a/tests/Integration/assets/MergeTestData/Datum.yml +++ b/tests/Integration/assets/MergeTestData/Datum.yml @@ -17,6 +17,15 @@ lookup_options: merge_options: knockout_prefix: -- + LocalUsers: + merge_hash: deep + LocalUsers\Users: + merge_hash_array: DeepTuple + merge_options: + knockout_prefix: -- + tuple_keys: + - UserName + NetworkIpConfigurationMerged: merge_hash: deep merge_options: @@ -31,3 +40,27 @@ lookup_options: merge_basetype_array: Unique merge_options: knockout_prefix: -- + + RegistryValues: + merge_hash: deep + RegistryValues\Values: + merge_hash_array: UniqueKeyValTuples + merge_options: + knockout_prefix: -- + tuple_keys: + - Key + - ValueName + + SecurityOptions: + merge_hash: deep + SecurityOptions\Policies: + merge_hash_array: UniqueKeyValTuples + merge_options: + knockout_prefix: -- + tuple_keys: + - Name + + SummaryConfig: + merge_hash: deep + SummaryConfig\SumItems: + merge_hash_array: Sum diff --git a/tests/Integration/assets/MergeTestData/Roles/FileServer.yml b/tests/Integration/assets/MergeTestData/Roles/FileServer.yml index b529a9d..18a4cba 100644 --- a/tests/Integration/assets/MergeTestData/Roles/FileServer.yml +++ b/tests/Integration/assets/MergeTestData/Roles/FileServer.yml @@ -31,3 +31,30 @@ NetworkIpConfigurationNonMerged: Gateway: 192.168.10.50 DnsServer: 192.168.10.10 DisableNetbios: True + +RegistryValues: + Values: + - Key: --HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + ValueName: --Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: UserServiceFlags + ValueData: 0 + ValueType: Dword + Ensure: Present + Force: true + +SummaryConfig: + SumItems: + - ItemName: Item1 + ItemNumber: 3 + Decription: Item1 with number 3 diff --git a/tests/Integration/assets/MergeTestData/Roles/WebServer.yml b/tests/Integration/assets/MergeTestData/Roles/WebServer.yml index 42e8e42..bb14fed 100644 --- a/tests/Integration/assets/MergeTestData/Roles/WebServer.yml +++ b/tests/Integration/assets/MergeTestData/Roles/WebServer.yml @@ -1,5 +1,7 @@ Configurations: - WindowsFeatures + - NetworkIpConfigurationMerged + - LocalUsers WindowsFeatures: Name: @@ -18,6 +20,7 @@ NetworkIpConfigurationMerged: - 192.168.12.0/24 - 192.168.23.0/24 - 192.168.34.0/24 + - InterfaceAlias: --Ethernet 2 NetworkIpConfigurationNonMerged: ConfigureIPv6: -1 @@ -27,3 +30,29 @@ NetworkIpConfigurationNonMerged: Gateway: 192.168.10.50 DnsServer: 192.168.10.10 DisableNetbios: True + +LocalUsers: + Users: + - UserName: --LocalAdmin + DependsOn: + - '--[SecurityOption]secPolOpt_Accounts_Rename_administrator_account::[SecurityOptions]SecurityOptions' + +RegistryValues: + Values: + - Key: --HKLM:\SYSTEM\CurrentControlSet\Services\DevicesPickerUserSvc + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: Start + ValueData: 4 + ValueType: Dword + Ensure: Present + Force: true + - Key: HKLM:\SYSTEM\CurrentControlSet\Services\CaptureService + ValueName: UserServiceFlags + ValueData: 0 + ValueType: Dword + Ensure: Present + Force: true + +SecurityOptions: + Policies: + - Name: --Accounts_Rename_administrator_account