From 3bdf435eddc997284677918aad0d5785b66c1318 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 2 May 2020 08:17:25 +0200 Subject: [PATCH] BREAKING CHANGE: Adds cmdlets needed for NetworkingDsc (#28) - Added cmdlet ConvertTo-CimInstance. _This cmdlet comes from NetworkingDsc._ - Added cmdlet ConvertTo-Hashtable. _This cmdlet comes from NetworkingDsc._ - Update the README.md with new cmdlet documentation format. following the style guideline. - Update to use HQRM tests from the DscResource.Test module. - Update the repository to use the latest version of ModuleBuilder. - Update to use the latest pipeline files. - BREAKING CHANGE: Updated the cmdlet Test-DscParameterState to match the one in the module NetworkingDsc which have been extended with for example checking credentials and types. This might be a breaking change in certain scenarios, for example the type checking if on by default. This change is required to be able to move the module NetworkingDsc to use this module. --- .gitignore | 9 +- CHANGELOG.md | 19 +- CONTRIBUTING.md | 4 + GitVersion.yml | 14 +- README.md | 149 ++- RequiredModules.psd1 | 9 +- Resolve-Dependency.psd1 | 9 +- azure-pipelines.yml | 184 +++- build.ps1 | 38 +- build.yaml | 68 +- source/DscResource.Common.psm1 | 2 +- source/Public/Assert-BoundParameter.ps1 | 20 + source/Public/Assert-IPAddress.ps1 | 32 +- source/Public/Assert-Module.ps1 | 3 + source/Public/ConvertTo-CimInstance.ps1 | 54 + source/Public/ConvertTo-HashTable.ps1 | 68 ++ source/Public/Get-LocalizedData.ps1 | 7 + source/Public/Get-TemporaryFolder.ps1 | 8 + .../Public/New-InvalidArgumentException.ps1 | 9 + .../Public/New-InvalidOperationException.ps1 | 14 + source/Public/New-InvalidResultException.ps1 | 18 + source/Public/New-NotImplementedException.ps1 | 13 + source/Public/New-ObjectNotFoundException.ps1 | 14 + source/Public/Remove-CommonParameter.ps1 | 45 + source/Public/Test-DscParameterState.ps1 | 415 ++++++-- source/Public/Test-IsNanoServer.ps1 | 9 + source/en-US/DscResource.Common.psd1 | 17 - source/en-US/DscResource.Common.strings.psd1 | 25 + source/prefix.ps1 | 2 +- source/suffix.ps1 | 1 - .../Public/ConvertTo-CimInstance.Tests.ps1 | 60 ++ .../Unit/Public/ConvertTo-Hashtable.Tests.ps1 | 56 + .../Public/Remove-CommonParameter.Tests.ps1 | 67 ++ .../Public/Test-DscParameterState.Tests.ps1 | 999 ++++++++++++++---- 34 files changed, 1923 insertions(+), 538 deletions(-) create mode 100644 source/Public/ConvertTo-CimInstance.ps1 create mode 100644 source/Public/ConvertTo-HashTable.ps1 create mode 100644 source/Public/Remove-CommonParameter.ps1 delete mode 100644 source/en-US/DscResource.Common.psd1 create mode 100644 source/en-US/DscResource.Common.strings.psd1 create mode 100644 tests/Unit/Public/ConvertTo-CimInstance.Tests.ps1 create mode 100644 tests/Unit/Public/ConvertTo-Hashtable.Tests.ps1 create mode 100644 tests/Unit/Public/Remove-CommonParameter.Tests.ps1 diff --git a/.gitignore b/.gitignore index 186edcf..1733276 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,3 @@ -.vscode +.vscode .vs output/ -modules/* -.kitchen/* -**.bak -.kitchen.local.yml - -!**/README.md -DscResource.tests diff --git a/CHANGELOG.md b/CHANGELOG.md index 05cf0d5..799d1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - - Added the cmdlet `Assert-IPAddress` + +- Added the cmdlet `Assert-IPAddress` +- Added the cmdlet `ConvertTo-CimInstance`. _This cmdlet comes from NetworkingDsc._ +- Added the cmdlet `ConvertTo-Hashtable`. _This cmdlet comes from NetworkingDsc._ ### Changed - Update the README.md with new cmdlet documentation format. - -### Fixed - -- The code in the unit tests for `Test-DscParameterState` is now closer - following the style guideline. +- Update to use HQRM tests from the DscResource.Test module. +- Update the repository to use the latest version of ModuleBuilder. +- Update to use the latest pipeline files. +- BREAKING CHANGE: Updated the cmdlet `Test-DscParameterState` to match + the one in the module NetworkingDsc which have been extended with for + example checking credentials and types. This might be a breaking change + in certain scenarios, for example the type checking if on by default. + _This change is required to be able to move the module NetworkingDsc_ + _to use this module._ ## [0.6.0] - 2020-04-23 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6895ab..3544bcc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,7 @@ # Contributing Please check out common DSC Community [contributing guidelines](https://dsccommunity.org/guidelines/contributing). + +## Running the Tests + +If want to know how to run this module's tests you can look at the [Testing Guidelines](https://dsccommunity.org/guidelines/testing-guidelines/#running-tests) diff --git a/GitVersion.yml b/GitVersion.yml index 986c4f3..51c9904 100644 --- a/GitVersion.yml +++ b/GitVersion.yml @@ -1,5 +1,5 @@ mode: ContinuousDelivery -next-version: 0.1 +next-version: 0.0.1 major-version-bump-message: '\s?(breaking|major|breaking\schange)' minor-version-bump-message: '(adds?|features?|minor)\b' patch-version-bump-message: '\s?(fix|patch)' @@ -24,15 +24,3 @@ branches: ignore: sha: [] merge-message-formats: {} - - -# feature: -# tag: useBranchName -# increment: Minor -# regex: f(eature(s)?)?[/-] -# source-branches: ['master'] -# hotfix: -# tag: fix -# increment: Patch -# regex: (hot)?fix(es)?[/-] -# source-branches: ['master'] diff --git a/README.md b/README.md index c334770..29f36a7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Please check out common DSC Community [contributing guidelines](https://dsccommu ## How to implement -See the article [DscResource.Common functions in a DSC module](/blog/use-dscresource-common-functions-in-module/) +See the article [DscResource.Common functions in a DSC module](https://dsccommunity.org/blog/use-dscresource-common-functions-in-module/) describing how to convert a DSC resource module to use DscResource.Common. ## Cmdlets @@ -74,6 +74,45 @@ Assert-BoundParameter @assertBoundParameterParameters This example throws an exception if `$PSBoundParameters` contains both the parameters `Parameter1` and `Parameter2`. +### `Assert-IPAddress` + +Asserts if the IP Address is valid and optionally validates +the IP Address against an Address Family + +### Syntax + +```plaintext +Assert-IPAddress [-Address] [[-AddressFamily] ] [] +``` + +#### Outputs + +None. + +#### Example + +```powershell +Assert-IPAddress -Address '127.0.0.1' +``` + +This will assert that the supplied address is a valid IPv4 address. +If it is not an exception will be thrown. + +```powershell +Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' +``` + +This will assert that the supplied address is a valid IPv6 address. +If it is not an exception will be thrown. + +```powershell +Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' -AddressFamily 'IPv6' +``` + +This will assert that address is valid and that it matches the +supplied address family. If the supplied address family does not match +the address an exception will be thrown. + ### `Assert-Module` Assert if the specific module is available to be imported and optionally @@ -106,36 +145,77 @@ This will assert that the module DhcpServer is available and that it has been imported into the session. If the module is not available an exception will be thrown. -### `Assert-IPAddress` -Asserts if the IP Address is valid and optionally validates -the IP Address against an Address Family +### `ConvertTo-CimInstance` -### Syntax +This function is used to convert a hashtable into MSFT_KeyValuePair objects. +These are stored as an CimInstance array. DSC cannot handle hashtables but +CimInstances arrays storing MSFT_KeyValuePair. + +#### Syntax ```plaintext -Assert-IPAddress [-Address] [[-AddressFamily] ] [] +ConvertTo-CimInstance -Hashtable [] ``` -#### Outputs +### Outputs -None. +**System.Object[]** -#### Example +### Example ```powershell -Assert-IPAddress -Address '127.0.0.1' +ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' +} ``` -This will assert that the supplied address is a valid IPv4 or IPv6 address. -If it is not an exception will be thrown. +This example returns an CimInstance with the provided hashtable values. + +### `ConvertTo-HashTable` + +This function is used to convert a CimInstance array containing +MSFT_KeyValuePair objects into a hashtable. + +#### Syntax + +```plaintext +ConvertTo-HashTable -CimInstance + [] +``` + +### Outputs + +**System.Collections.Hashtable** + +### Example ```powershell -Assert-Module -Address 'fe80:ab04:30F5:002b::1' AddressFamily = 'IPv6' +$newInstanceParameters = @{ + ClassName = 'MSFT_KeyValuePair' + Namespace = 'root/microsoft/Windows/DesiredStateConfiguration' + ClientOnly = $true +} + +$cimInstance = [Microsoft.Management.Infrastructure.CimInstance[]] ( + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'FirstName' + Value = 'John' + }), + + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'LastName' + Value = 'Smith' + }) +) + +ConvertTo-HashTable -CimInstance $cimInstance ``` -This will assert that address is valid and that it matches the -supplied address family. If the supplied address family does not match -the address an exception will be thrown. +This creates a array om CimInstances of the class name MSFT_KeyValuePair +and passes it to ConvertTo-HashTable which returns a hashtable. ### `Get-LocalizedData` @@ -355,9 +435,31 @@ catch $errorMessage = $script:localizedData.PathNotFoundMessage -f $path New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ } +``` + +### `Remove-CommonParameter` + +This function serves the purpose of removing common parameters and option +common parameters from a parameter hashtable. + +#### Syntax + +```plaintext +Remove-CommonParameter [-Hashtable] [] +``` + +### Outputs + +**System.Collections.Hashtable** +### Example + +```powershell +Remove-CommonParameter -Hashtable $PSBoundParameters ``` +Returns a new hashtable without the common and optional common parameters. + ### `Test-DscParameterState` This function is used to compare the values in the current state against @@ -367,8 +469,9 @@ the desired values for any DSC resource. ```plaintext -Test-DscParameterState [-CurrentValues] [-DesiredValues] [[-ValuesToCheck] ] - [] +Test-DscParameterState [-CurrentValues] [-DesiredValues] + [[-ValuesToCheck] ] [-TurnOffTypeChecking] [-ReverseCheck] + [-SortArrayValues] [] ``` @@ -388,9 +491,9 @@ $returnValue = Test-DscParameterState -CurrentValues $currentState -DesiredValue ``` -`Get-TargetResource` is called first using all bound parameters to get -the values in the current state. The result is then compared to the desired -state by calling `Test-DscParameterState`. +The function `Get-TargetResource` is called first using all bound parameters +to get the values in the current state. The result is then compared to the +desired state by calling `Test-DscParameterState`. ##### Example 2 @@ -411,8 +514,8 @@ $returnValue = Test-DscParameterState ` ``` This compares the values in the current state against the desires state. -`Get-TargetResource` is called using just the required parameters to get -the values in the current state. +The function `Get-TargetResource` is called using just the required parameters +to get the values in the current state. ### `Test-IsNanoServer` diff --git a/RequiredModules.psd1 b/RequiredModules.psd1 index 8a07004..14a383c 100644 --- a/RequiredModules.psd1 +++ b/RequiredModules.psd1 @@ -1,20 +1,21 @@ @{ - # Set up a mini virtual environment... PSDependOptions = @{ AddToPath = $true Target = 'output\RequiredModules' Parameters = @{ + Repository = '' } } - invokeBuild = 'latest' + InvokeBuild = 'latest' PSScriptAnalyzer = 'latest' - pester = 'latest' + Pester = 'latest' Plaster = 'latest' - ModuleBuilder = '1.0.0' + ModuleBuilder = 'latest' ChangelogManagement = 'latest' Sampler = 'latest' MarkdownLinkCheck = 'latest' 'DscResource.AnalyzerRules' = 'latest' xDscResourceDesigner = 'latest' + 'DscResource.Test' = 'latest' } diff --git a/Resolve-Dependency.psd1 b/Resolve-Dependency.psd1 index d193a17..2ae8c0d 100644 --- a/Resolve-Dependency.psd1 +++ b/Resolve-Dependency.psd1 @@ -1,10 +1,5 @@ -@{ # Defaults Parameter value to be loaded by the Resolve-Dependency command (unless set in Bound Parameters) - #PSDependTarget = './output/modules' - #Proxy = '' - #ProxyCredential = '$MyCredentialVariable' #TODO: find a way to support credentials in build (resolve variable) +@{ Gallery = 'PSGallery' - # AllowOldPowerShellGetModule = $true - #MinimumPSDependVersion = '0.3.0' AllowPrerelease = $false - WithYAML = $true # Will also bootstrap PowerShell-Yaml to read other config files + WithYAML = $true } diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6fcfe28..498c1bd 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -10,157 +10,231 @@ trigger: - "v*" exclude: - "*-*" -#variables: +variables: + buildFolderName: output + buildArtifactName: output + testResultFolderName: testResults + testArtifactName: testResults stages: - - stage: Build_artefact + - stage: Build jobs: - - job: BuildModuleJob + - job: Package_Module + displayName: 'Package Module' pool: vmImage: 'ubuntu 16.04' steps: - task: GitVersion@5 - name: gitversion + name: gitVersion + displayName: 'Evaluate Next Version' inputs: runtime: 'core' configFilePath: 'GitVersion.yml' - - task: PowerShell@2 - name: Build + name: package + displayName: 'Build & Package Module' inputs: filePath: './build.ps1' arguments: '-ResolveDependency -tasks pack' pwsh: true env: - ModuleVersion: $(GitVersion.Informationalversion) - + ModuleVersion: $(gitVersion.NuGetVersionV2) - task: PublishBuildArtifacts@1 + displayName: 'Publish Build Artifact' inputs: - PathtoPublish: 'output/' - ArtifactName: 'output' + pathToPublish: 'output/' + artifactName: 'output' publishLocation: 'Container' - - stage: test_module - dependsOn: Build_artefact + - stage: Test + dependsOn: Build jobs: + - job: Test_HQRM + displayName: 'HQRM' + pool: + vmImage: 'windows-2019' + timeoutInMinutes: 0 + steps: + - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: 'output' + downloadPath: '$(Build.SourcesDirectory)' + - task: PowerShell@2 + name: test + displayName: 'Run HQRM Test' + inputs: + filePath: './build.ps1' + arguments: '-Tasks hqrmtest' + pwsh: false + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'HQRM' + - job: test_linux + displayName: 'Linux' + timeoutInMinutes: 0 pool: vmImage: 'ubuntu 16.04' steps: - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' inputs: buildType: 'current' downloadType: 'single' artifactName: 'output' downloadPath: '$(Build.SourcesDirectory)' - - task: PowerShell@2 - name: Test + name: test + displayName: 'Run Tests' inputs: filePath: './build.ps1' arguments: '-tasks test' - - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() inputs: testResultsFormat: 'NUnit' - testResultsFiles: 'output/testResults/NUnit*.xml' - + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Linux' - # Publish code coverage results - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: 'JaCoCo' - summaryFileLocation: 'output/testResults/CodeCov*.xml' - job: test_windows_core + displayName: 'Windows (PowerShell Core)' + timeoutInMinutes: 0 pool: vmImage: 'windows-2019' steps: - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' inputs: buildType: 'current' downloadType: 'single' artifactName: 'output' downloadPath: '$(Build.SourcesDirectory)' - - task: PowerShell@2 - name: Test + name: test + displayName: 'Run Tests' inputs: filePath: './build.ps1' arguments: '-tasks test' pwsh: true - - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() inputs: testResultsFormat: 'NUnit' - testResultsFiles: 'output/testResults/NUnit*.xml' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Windows Server (PowerShell Core)' - - # Publish code coverage results - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: 'JaCoCo' - summaryFileLocation: 'output/testResults/CodeCov*.xml' - job: test_windows_ps + displayName: 'Windows (Windows PowerShell)' + timeoutInMinutes: 0 pool: vmImage: 'windows-2019' steps: - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' inputs: buildType: 'current' downloadType: 'single' artifactName: 'output' downloadPath: '$(Build.SourcesDirectory)' - - task: PowerShell@2 - name: Test + name: test + displayName: 'Run Tests' inputs: filePath: './build.ps1' arguments: '-tasks test' pwsh: false - - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() inputs: testResultsFormat: 'NUnit' - testResultsFiles: 'output/testResults/NUnit*.xml' - - - # Publish code coverage results - - task: PublishCodeCoverageResults@1 + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'Windows Server (Windows PowerShell)' + - task: PublishBuildArtifacts@1 + displayName: 'Publish Test Artifact' inputs: - codeCoverageTool: 'JaCoCo' - summaryFileLocation: 'output/testResults/CodeCov*.xml' + pathToPublish: '$(buildFolderName)/$(testResultFolderName)/' + artifactName: $(testArtifactName) + publishLocation: 'Container' + - job: test_macos + displayName: 'macOS' + timeoutInMinutes: 0 pool: vmImage: 'macos-latest' steps: - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' inputs: buildType: 'current' downloadType: 'single' artifactName: 'output' downloadPath: '$(Build.SourcesDirectory)' - - task: PowerShell@2 - name: Test + name: test + displayName: 'Run Tests' inputs: filePath: './build.ps1' arguments: '-tasks test' pwsh: true - - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() inputs: testResultsFormat: 'NUnit' - testResultsFiles: 'output/testResults/NUnit*.xml' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit*.xml' + testRunTitle: 'MacOS' - # Publish code coverage results + - job: Code_Coverage + displayName: 'Publish Code Coverage' + dependsOn: test_windows_ps + pool: + vmImage: 'ubuntu 16.04' + timeoutInMinutes: 0 + steps: + - pwsh: | + $repositoryOwner,$repositoryName = $env:BUILD_REPOSITORY_NAME -split '/' + echo "##vso[task.setvariable variable=RepositoryOwner;isOutput=true]$repositoryOwner" + echo "##vso[task.setvariable variable=RepositoryName;isOutput=true]$repositoryName" + name: dscBuildVariable + displayName: 'Set Environment Variables' + - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: $(buildArtifactName) + downloadPath: '$(Build.SourcesDirectory)' + - task: DownloadBuildArtifacts@0 + displayName: 'Download Test Artifact' + inputs: + buildType: 'current' + downloadType: 'single' + artifactName: $(testArtifactName) + downloadPath: '$(Build.SourcesDirectory)/$(buildFolderName)' - task: PublishCodeCoverageResults@1 + displayName: 'Publish Azure Code Coverage' + condition: succeededOrFailed() inputs: codeCoverageTool: 'JaCoCo' - summaryFileLocation: 'output/testResults/CodeCov*.xml' + summaryFileLocation: '$(buildFolderName)/$(testResultFolderName)/JaCoCo_coverage.xml' + pathToSources: '$(Build.SourcesDirectory)/$(buildFolderName)/$(dscBuildVariable.RepositoryName)' + - script: | + bash <(curl -s https://codecov.io/bash) -f "./$(buildFolderName)/$(testResultFolderName)/JaCoCo_coverage.xml" -t $CODECOV_TOKEN + displayName: 'Upload to Codecov.io' + condition: succeededOrFailed() - stage: Deploy - dependsOn: test_module - # Only execute deploy stage if we're on master and previous stage succeeded + dependsOn: Test condition: | and( succeeded(), @@ -171,18 +245,21 @@ stages: contains(variables['System.TeamFoundationCollectionUri'], 'dsccommunity') ) jobs: - - job: Deploy_Artefact + - job: Deploy_Module + displayName: 'Deploy Module' pool: vmImage: 'ubuntu 16.04' steps: - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifact' inputs: buildType: 'current' downloadType: 'single' artifactName: 'output' downloadPath: '$(Build.SourcesDirectory)' - task: PowerShell@2 - name: publish_prerelease + name: publishRelease + displayName: 'Publish Release' inputs: filePath: './build.ps1' arguments: '-tasks publish' @@ -191,7 +268,8 @@ stages: GitHubToken: $(GitHubToken) GalleryApiToken: $(GalleryApiToken) - task: PowerShell@2 - name: send_changelog_PR + name: sendChangelogPR + displayName: 'Send Changelog PR' inputs: filePath: './build.ps1' arguments: '-tasks Create_ChangeLog_GitHub_PR' diff --git a/build.ps1 b/build.ps1 index a94f13b..4630cb8 100644 --- a/build.ps1 +++ b/build.ps1 @@ -18,7 +18,7 @@ param [validateScript( { Test-Path -Path $_ } )] - $BuildConfig = './build.yaml', + $BuildConfig, [Parameter()] # A Specific folder to build the artefact into. @@ -35,22 +35,34 @@ param [Parameter()] $RequiredModulesDirectory = $(Join-Path 'output' 'RequiredModules'), + [Parameter()] + [object[]] + $PesterScript, + # Filter which tags to run when invoking Pester tests # This is used in the Invoke-Pester.pester.build.ps1 tasks [Parameter()] [string[]] $PesterTag, - [Parameter()] - [string[]] - $PesterScript, - # Filter which tags to exclude when invoking Pester tests # This is used in the Invoke-Pester.pester.build.ps1 tasks [Parameter()] [string[]] $PesterExcludeTag, + # Filter which tags to run when invoking DSC Resource tests + # This is used in the DscResource.Test.build.ps1 tasks + [Parameter()] + [string[]] + $DscTestTag, + + # Filter which tags to exclude when invoking DSC Resource tests + # This is used in the DscResource.Test.build.ps1 tasks + [Parameter()] + [string[]] + $DscTestExcludeTag, + [Parameter()] [Alias('bootstrap')] [switch]$ResolveDependency, @@ -210,6 +222,22 @@ process Begin { + # Find build config if not specified + if (-not $BuildConfig) { + $config = Get-ChildItem -Path "$PSScriptRoot\*" -Include 'build.y*ml', 'build.psd1', 'build.json*' -ErrorAction:Ignore + if (-not $config -or ($config -is [array] -and $config.Length -le 0)) { + throw "No build configuration found. Specify path via -BuildConfig" + } + elseif ($config -is [array]) { + if ($config.Length -gt 1) { + throw "More than one build configuration found. Specify which one to use via -BuildConfig" + } + $BuildConfig = $config[0] + } + else { + $BuildConfig = $config + } + } # Bootstrapping the environment before using Invoke-Build as task runner if ($MyInvocation.ScriptName -notLike '*Invoke-Build.ps1') diff --git a/build.yaml b/build.yaml index b343123..24a6d34 100644 --- a/build.yaml +++ b/build.yaml @@ -2,38 +2,18 @@ #################################################### # ModuleBuilder Configuration # #################################################### -# Path to the Module Manifest to build (where path will be resolved from) -# SourcePath: ./Sampler/Sampler.psd1 -# Output Directory where ModuleBuilder will build the Module, relative to module manifest -# OutputDirectory: ../output/Sampler CopyDirectories: - en-US -# SemVer: '1.2.3' prefix: prefix.ps1 -# Suffix to add to Root module PSM1 after merge (here, the Set-Alias exporting IB tasks) suffix: suffix.ps1 +Encoding: UTF8 VersionedOutputDirectory: true #################################################### -# ModuleBuilder Submodules Configuration # +# Pipeline Configuration # #################################################### - -# NestedModule: -# HelperSubmodule: # This is the first submodule to build into the output -# Path: ./Sampler/Modules/HelperSubmodule/HelperSubmodule.psd1 -# # $ModuleVersionFolder is trimmed (remove -.*) and OutputDirectory expanded (the only one) -# OutputDirectory: ./output/Sampler/$ModuleVersionFolder/Modules/HelperSubmodule -# VersionedOutputDirectory: false -# # suffix: -# # prefix: - - -#################################################### -# Sampler Pipeline Configuration # -#################################################### -# Defining 'Workflows' (suite of InvkeBuild tasks) to be run using their alias BuildWorkflow: - '.': # "." is the default Invoke-Build workflow. It is called when no -Tasks is specified to the build.ps1 + '.': - build - test @@ -47,53 +27,51 @@ BuildWorkflow: - build - package_module_nupkg - # defining test task to be run when invoking `./build.ps1 -Tasks test` + hqrmtest: + - DscResource_Tests_Stop_On_Fail + test: - Pester_Tests_Stop_On_Fail - Pester_if_Code_Coverage_Under_Threshold publish: - Publish_release_to_GitHub - - publish_module_to_gallery # runs if nuget is not available + - publish_module_to_gallery #################################################### # PESTER Configuration # #################################################### -Pester: #Passthru, OutputFile, CodeCoverageOutputFile not supported +Pester: OutputFormat: NUnitXML - # Will look at every *.ps1 & *.psm1 under ModulePath, excepts when $_.FullName -match (Join-Path $ProjectPath $ExcludeFromCodeCoverageItem) - # ExcludeFromCodeCoverage: - # Default is to use the tests folder in the project folder or source folder (if present) - # can use it to prioritize: tests/QA, tests/Unit, tests/Integration + ExcludeFromCodeCoverage: Script: - # - tests/QA/module.tests.ps1 - # - tests/QA - # - tests/Unit - # - tests/Integration + - tests/QA + - tests/Unit ExcludeTag: - - helpQuality Tag: - CodeCoverageThreshold: 50 # Set to 0 to bypass + CodeCoverageThreshold: 50 + CodeCoverageOutputFile: JaCoCo_coverage.xml + CodeCoverageOutputFileEncoding: ascii +DscTest: + ExcludeTag: + - 'Common Tests - New Error-Level Script Analyzer Rules' + Tag: + ExcludeSourceFile: + - output + ExcludeModuleFile: -Resolve-Dependency: #Parameters for Resolve-Dependency - #PSDependTarget: ./output/modules - #Proxy: '' - #ProxyCredential: +Resolve-Dependency: Gallery: 'PSGallery' - # AllowOldPowerShellGetModule: true - #MinimumPSDependVersion = '0.3.0' AllowPrerelease: false Verbose: false ModuleBuildTasks: - # - ModuleName: 'alias to search' Sampler: - - '*.build.Sampler.ib.tasks' # this means: import (dot source) all aliases ending with .ib.tasks exported by sampler module + - '*.build.Sampler.ib.tasks' -# Invoke-Build Header to be used to 'decorate' the terminal output of the tasks. TaskHeader: | param($Path) "" diff --git a/source/DscResource.Common.psm1 b/source/DscResource.Common.psm1 index 366e01e..223917b 100644 --- a/source/DscResource.Common.psm1 +++ b/source/DscResource.Common.psm1 @@ -1 +1 @@ -# This file will be generated \ No newline at end of file +# This file will be generated automatically by ModuleBuilder. diff --git a/source/Public/Assert-BoundParameter.ps1 b/source/Public/Assert-BoundParameter.ps1 index 179d666..cdf7db9 100644 --- a/source/Public/Assert-BoundParameter.ps1 +++ b/source/Public/Assert-BoundParameter.ps1 @@ -3,6 +3,10 @@ Throws an error if there is a bound parameter that exists in both the mutually exclusive lists. + .DESCRIPTION + Throws an error if there is a bound parameter that exists in both the + mutually exclusive lists. + .PARAMETER BoundParameterList The parameters that should be evaluated against the mutually exclusive lists MutuallyExclusiveList1 and MutuallyExclusiveList2. This parameter is @@ -15,6 +19,22 @@ .PARAMETER MutuallyExclusiveList2 An array of parameter names that are not allowed to be bound at the same time as those in MutuallyExclusiveList1. + + .EXAMPLE + $assertBoundParameterParameters = @{ + BoundParameterList = $PSBoundParameters + MutuallyExclusiveList1 = @( + 'Parameter1' + ) + MutuallyExclusiveList2 = @( + 'Parameter2' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + This example throws an exception if `$PSBoundParameters` contains both + the parameters `Parameter1` and `Parameter2`. #> function Assert-BoundParameter { diff --git a/source/Public/Assert-IPAddress.ps1 b/source/Public/Assert-IPAddress.ps1 index 7056dda..6c9ca96 100644 --- a/source/Public/Assert-IPAddress.ps1 +++ b/source/Public/Assert-IPAddress.ps1 @@ -1,14 +1,36 @@ <# .SYNOPSIS - Check the Address details are valid and do not conflict with Address family. - If any problems are detected an exception will be thrown. + Asserts that the specified IP address is valid. + + .DESCRIPTION + Checks the IP address so that it is valid and do not conflict with address + family. If any problems are detected an exception will be thrown. .PARAMETER AddressFamily - IP address family (IPv4 or IPv6) that the supplied Address should be in. - Valid values are 'IPv4' or 'IPv6'. + IP address family that the supplied Address should be in. Valid values are + 'IPv4' or 'IPv6'. .PARAMETER Address - IPv4 or IPv6 Address. + Specifies an IPv4 or IPv6 address. + + .EXAMPLE + Assert-IPAddress -Address '127.0.0.1' + + This will assert that the supplied address is a valid IPv4 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' + + This will assert that the supplied address is a valid IPv6 address. + If it is not an exception will be thrown. + + .EXAMPLE + Assert-IPAddress -Address 'fe80:ab04:30F5:002b::1' -AddressFamily 'IPv6' + + This will assert that address is valid and that it matches the + supplied address family. If the supplied address family does not match + the address an exception will be thrown. #> function Assert-IPAddress { diff --git a/source/Public/Assert-Module.ps1 b/source/Public/Assert-Module.ps1 index 4df1f5f..3310881 100644 --- a/source/Public/Assert-Module.ps1 +++ b/source/Public/Assert-Module.ps1 @@ -2,6 +2,9 @@ .SYNOPSIS Assert if the specific module is available to be imported. + .DESCRIPTION + Assert if the specific module is available to be imported. + .PARAMETER ModuleName Specifies the name of the module to assert. diff --git a/source/Public/ConvertTo-CimInstance.ps1 b/source/Public/ConvertTo-CimInstance.ps1 new file mode 100644 index 0000000..7189853 --- /dev/null +++ b/source/Public/ConvertTo-CimInstance.ps1 @@ -0,0 +1,54 @@ +<# + .SYNOPSIS + Converts a hashtable into a CimInstance array. + + .DESCRIPTION + This function is used to convert a hashtable into MSFT_KeyValuePair objects. + These are stored as an CimInstance array. DSC cannot handle hashtables but + CimInstances arrays storing MSFT_KeyValuePair. + + .PARAMETER Hashtable + A hashtable with the values to convert. + + .OUTPUTS + An object array with CimInstance objects. + + .EXAMPLE + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + + This example returns an CimInstance with the provided hashtable values. +#> +function ConvertTo-CimInstance +{ + [CmdletBinding()] + [OutputType([System.Object[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'Hashtable')] + [System.Collections.Hashtable] + $Hashtable + ) + + process + { + foreach ($item in $Hashtable.GetEnumerator()) + { + New-CimInstance -ClassName 'MSFT_KeyValuePair' -Namespace 'root/microsoft/Windows/DesiredStateConfiguration' -Property @{ + Key = $item.Key + Value = if ($item.Value -is [array]) + { + $item.Value -join ',' + } + else + { + $item.Value + } + } -ClientOnly + } + } +} diff --git a/source/Public/ConvertTo-HashTable.ps1 b/source/Public/ConvertTo-HashTable.ps1 new file mode 100644 index 0000000..e456947 --- /dev/null +++ b/source/Public/ConvertTo-HashTable.ps1 @@ -0,0 +1,68 @@ +<# + .SYNOPSIS + Converts CimInstances into a hashtable. + + .DESCRIPTION + This function is used to convert a CimInstance array containing + MSFT_KeyValuePair objects into a hashtable. + + .PARAMETER CimInstance + An array of CimInstances or a single CimInstance object to convert. + + .OUTPUTS + Hashtable + + .EXAMPLE + $newInstanceParameters = @{ + ClassName = 'MSFT_KeyValuePair' + Namespace = 'root/microsoft/Windows/DesiredStateConfiguration' + ClientOnly = $true + } + + $cimInstance = [Microsoft.Management.Infrastructure.CimInstance[]] ( + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'FirstName' + Value = 'John' + }), + + (New-CimInstance @newInstanceParameters -Property @{ + Key = 'LastName' + Value = 'Smith' + }) + ) + + ConvertTo-HashTable -CimInstance $cimInstance + + This creates a array om CimInstances of the class name MSFT_KeyValuePair + and passes it to ConvertTo-HashTable which returns a hashtable. +#> +function ConvertTo-HashTable +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'CimInstance')] + [AllowEmptyCollection()] + [Microsoft.Management.Infrastructure.CimInstance[]] + $CimInstance + ) + + begin + { + $result = @{ } + } + + process + { + foreach ($ci in $CimInstance) + { + $result.Add($ci.Key, $ci.Value) + } + } + + end + { + $result + } +} diff --git a/source/Public/Get-LocalizedData.ps1 b/source/Public/Get-LocalizedData.ps1 index d485dc4..16dbf4c 100644 --- a/source/Public/Get-LocalizedData.ps1 +++ b/source/Public/Get-LocalizedData.ps1 @@ -129,6 +129,13 @@ localized message is displayed. For more information, see about_Script_Internationalization. + + .EXAMPLE + $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + + This is an example that can be used in DSC resources to import the + localized strings and if the current UI culture localized folder does + not exist the UI culture 'en-US' is returned. #> function Get-LocalizedData { diff --git a/source/Public/Get-TemporaryFolder.ps1 b/source/Public/Get-TemporaryFolder.ps1 index f990447..5c22822 100644 --- a/source/Public/Get-TemporaryFolder.ps1 +++ b/source/Public/Get-TemporaryFolder.ps1 @@ -2,11 +2,19 @@ .SYNOPSIS Returns the path of the current user's temporary folder. + .DESCRIPTION + Returns the path of the current user's temporary folder. + .NOTES This is the same as doing the following - Windows: $env:TEMP - macOS: $env:TMPDIR - Linux: /tmp/ + + .EXAMPLE + Get-TemporaryFolder + + Returns the current user temporary folder on the current operating system. #> function Get-TemporaryFolder { diff --git a/source/Public/New-InvalidArgumentException.ps1 b/source/Public/New-InvalidArgumentException.ps1 index b97cff1..ff73fa9 100644 --- a/source/Public/New-InvalidArgumentException.ps1 +++ b/source/Public/New-InvalidArgumentException.ps1 @@ -2,11 +2,20 @@ .SYNOPSIS Creates and throws an invalid argument exception. + .DESCRIPTION + Creates and throws an invalid argument exception. + .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ArgumentName The name of the invalid argument that is causing this error to be thrown. + + .EXAMPLE + $errorMessage = $script:localizedData.ActionCannotBeUsedInThisContextMessage ` + -f $Action, $Parameter + + New-InvalidArgumentException -ArgumentName 'Action' -Message $errorMessage #> function New-InvalidArgumentException { diff --git a/source/Public/New-InvalidOperationException.ps1 b/source/Public/New-InvalidOperationException.ps1 index 1996d34..189dc9c 100644 --- a/source/Public/New-InvalidOperationException.ps1 +++ b/source/Public/New-InvalidOperationException.ps1 @@ -2,11 +2,25 @@ .SYNOPSIS Creates and throws an invalid operation exception. + .DESCRIPTION + Creates and throws an invalid operation exception. + .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Start-Process @startProcessArguments + } + catch + { + $errorMessage = $script:localizedData.InstallationFailedMessage -f $Path, $processId + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } #> function New-InvalidOperationException { diff --git a/source/Public/New-InvalidResultException.ps1 b/source/Public/New-InvalidResultException.ps1 index efe5497..4e8d27b 100644 --- a/source/Public/New-InvalidResultException.ps1 +++ b/source/Public/New-InvalidResultException.ps1 @@ -2,11 +2,29 @@ .SYNOPSIS Creates and throws an invalid result exception. + .DESCRIPTION + Creates and throws an invalid result exception. + .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + $numberOfObjects = Get-ChildItem -Path $path + if ($numberOfObjects -eq 0) + { + throw 'To few files.' + } + } + catch + { + $errorMessage = $script:localizedData.TooFewFilesMessage -f $path + New-InvalidResultException -Message $errorMessage -ErrorRecord $_ + } #> function New-InvalidResultException { diff --git a/source/Public/New-NotImplementedException.ps1 b/source/Public/New-NotImplementedException.ps1 index dc70b1a..aa3e6b6 100644 --- a/source/Public/New-NotImplementedException.ps1 +++ b/source/Public/New-NotImplementedException.ps1 @@ -2,11 +2,24 @@ .SYNOPSIS Creates and throws an not implemented exception. + .DESCRIPTION + Creates and throws an not implemented exception. + .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. + + .EXAMPLE + if ($runFeature) + { + $errorMessage = $script:localizedData.FeatureMissing -f $path + New-NotImplementedException -Message $errorMessage -ErrorRecord $_ + } + + Throws an not implemented exception if the variable $runFeature contains + a value. #> function New-NotImplementedException { diff --git a/source/Public/New-ObjectNotFoundException.ps1 b/source/Public/New-ObjectNotFoundException.ps1 index b118e0c..d85254d 100644 --- a/source/Public/New-ObjectNotFoundException.ps1 +++ b/source/Public/New-ObjectNotFoundException.ps1 @@ -3,11 +3,25 @@ .SYNOPSIS Creates and throws an object not found exception. + .DESCRIPTION + Creates and throws an object not found exception. + .PARAMETER Message The message explaining why this error is being thrown. .PARAMETER ErrorRecord The error record containing the exception that is causing this terminating error. + + .EXAMPLE + try + { + Get-ChildItem -Path $path + } + catch + { + $errorMessage = $script:localizedData.PathNotFoundMessage -f $path + New-ObjectNotFoundException -Message $errorMessage -ErrorRecord $_ + } #> function New-ObjectNotFoundException { diff --git a/source/Public/Remove-CommonParameter.ps1 b/source/Public/Remove-CommonParameter.ps1 new file mode 100644 index 0000000..48a9829 --- /dev/null +++ b/source/Public/Remove-CommonParameter.ps1 @@ -0,0 +1,45 @@ +<# + .SYNOPSIS + Removes common parameters from a hashtable. + + .DESCRIPTION + This function serves the purpose of removing common parameters and option + common parameters from a parameter hashtable. + + .PARAMETER Hashtable + The parameter hashtable that should be pruned. + + .EXAMPLE + Remove-CommonParameter -Hashtable $PSBoundParameters + + Returns a new hashtable without the common and optional common parameters. +#> +function Remove-CommonParameter +{ + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseShouldProcessForStateChangingFunctions', + '', + Justification = 'ShouldProcess is not supported in DSC resources.' + )] + [OutputType([System.Collections.Hashtable])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Hashtable + ) + + $inputClone = $Hashtable.Clone() + + $commonParameters = [System.Management.Automation.PSCmdlet]::CommonParameters + $commonParameters += [System.Management.Automation.PSCmdlet]::OptionalCommonParameters + + $Hashtable.Keys | Where-Object -FilterScript { + $_ -in $commonParameters + } | ForEach-Object -Process { + $inputClone.Remove($_) + } + + return $inputClone +} diff --git a/source/Public/Test-DscParameterState.ps1 b/source/Public/Test-DscParameterState.ps1 index ada1f56..6386dd0 100644 --- a/source/Public/Test-DscParameterState.ps1 +++ b/source/Public/Test-DscParameterState.ps1 @@ -1,27 +1,69 @@ - <# .SYNOPSIS This method is used to compare current and desired values for any DSC resource. + .DESCRIPTION + This function tests the parameter status of DSC resource parameters against + the current values present on the system. + .PARAMETER CurrentValues - This is hash table of the current values that are applied to the resource. + A hashtable with the current values on the system, obtained by e.g. + Get-TargetResource. .PARAMETER DesiredValues - This is a PSBoundParametersDictionary of the desired values for the resource. + The hashtable of desired values. For example $PSBoundParameters with the + desired values. .PARAMETER ValuesToCheck This is a list of which properties in the desired values list should be checked. If this is empty then all values in DesiredValues are checked. + + .PARAMETER TurnOffTypeChecking + Indicates that the type of the parameter should not be checked. + + .PARAMETER ReverseCheck + Indicates that a reverse check should be done. The current and desired state + are swapped for another test. + + .PARAMETER SortArrayValues + If the sorting of array values does not matter, values are sorted internally + before doing the comparison. + + .EXAMPLE + $currentState = Get-TargetResource @PSBoundParameters + + $returnValue = Test-DscParameterState -CurrentValues $currentState -DesiredValues $PSBoundParameters + + The function Get-TargetResource is called first using all bound parameters + to get the values in the current state. The result is then compared to the + desired state by calling `Test-DscParameterState`. + + .EXAMPLE + $getTargetResourceParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + Name = $Name + } + + $returnValue = Test-DscParameterState ` + -CurrentValues (Get-TargetResource @getTargetResourceParameters) ` + -DesiredValues $PSBoundParameters ` + -ValuesToCheck @( + 'FailsafeOperator' + 'NotificationMethod' + ) + + This compares the values in the current state against the desires state. + The function Get-TargetResource is called using just the required parameters + to get the values in the current state. #> function Test-DscParameterState { - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'returnValue')] [CmdletBinding()] - [OutputType([Boolean])] param ( [Parameter(Mandatory = $true)] - [System.Collections.Hashtable] + [System.Object] $CurrentValues, [Parameter(Mandatory = $true)] @@ -29,152 +71,295 @@ function Test-DscParameterState $DesiredValues, [Parameter()] - [System.Array] - $ValuesToCheck + [System.String[]] + $ValuesToCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $TurnOffTypeChecking, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $ReverseCheck, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SortArrayValues ) $returnValue = $true - if (($DesiredValues.GetType().Name -ne 'HashTable') ` - -and ($DesiredValues.GetType().Name -ne 'CimInstance') ` - -and ($DesiredValues.GetType().Name -ne 'PSBoundParametersDictionary')) + + if ($CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $CurrentValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $CurrentValues = ConvertTo-HashTable -CimInstance $CurrentValues + } + + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -or + $DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $DesiredValues = ConvertTo-HashTable -CimInstance $DesiredValues + } + + $types = 'System.Management.Automation.PSBoundParametersDictionary', 'System.Collections.Hashtable', 'Microsoft.Management.Infrastructure.CimInstance' + + if ($DesiredValues.GetType().FullName -notin $types) + { + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidDesiredValuesError -f $DesiredValues.GetType().FullName) ` + -ArgumentName 'DesiredValues' + } + + if ($CurrentValues.GetType().FullName -notin $types) { - $errorMessage = $script:localizedData.PropertyTypeInvalidForDesiredValues -f $($DesiredValues.GetType().Name) - New-InvalidArgumentException -ArgumentName 'DesiredValues' -Message $errorMessage + New-InvalidArgumentException ` + -Message ($script:localizedData.InvalidCurrentValuesError -f $CurrentValues.GetType().FullName) ` + -ArgumentName 'CurrentValues' } - if (($DesiredValues.GetType().Name -eq 'CimInstance') -and ($null -eq $ValuesToCheck)) + if ($DesiredValues -is [Microsoft.Management.Infrastructure.CimInstance] -and -not $ValuesToCheck) { - $errorMessage = $script:localizedData.PropertyTypeInvalidForValuesToCheck - New-InvalidArgumentException -ArgumentName 'ValuesToCheck' -Message $errorMessage + New-InvalidArgumentException ` + -Message $script:localizedData.InvalidValuesToCheckError ` + -ArgumentName 'ValuesToCheck' } - if (($null -eq $ValuesToCheck) -or ($ValuesToCheck.Count -lt 1)) + $desiredValuesClean = Remove-CommonParameter -Hashtable $DesiredValues + + if (-not $ValuesToCheck) { - $keyList = $DesiredValues.Keys + $keyList = $desiredValuesClean.Keys } else { $keyList = $ValuesToCheck } - $keyList | ForEach-Object -Process { - if (($_ -ne 'Verbose')) + foreach ($key in $keyList) + { + $desiredValue = $desiredValuesClean.$key + $currentValue = $CurrentValues.$key + + if ($desiredValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $desiredValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) { - if (($CurrentValues.ContainsKey($_) -eq $false) ` - -or ($CurrentValues.$_ -ne $DesiredValues.$_) ` - -or (($DesiredValues.GetType().Name -ne 'CimInstance' -and $DesiredValues.ContainsKey($_) -eq $true) -and ($null -ne $DesiredValues.$_ -and $DesiredValues.$_.GetType().IsArray))) + $desiredValue = ConvertTo-HashTable -CimInstance $desiredValue + } + if ($currentValue -is [Microsoft.Management.Infrastructure.CimInstance] -or + $currentValue -is [Microsoft.Management.Infrastructure.CimInstance[]]) + { + $currentValue = ConvertTo-HashTable -CimInstance $currentValue + } + + if ($null -ne $desiredValue) + { + $desiredType = $desiredValue.GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' + } + } + + if ($null -ne $currentValue) + { + $currentType = $currentValue.GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' + } + } + + if ($currentType.Name -ne 'Unknown' -and $desiredType.Name -eq 'PSCredential') + { + # This is a credential object. Compare only the user name + if ($currentType.Name -eq 'PSCredential' -and $currentValue.UserName -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + continue + } + else + { + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue.UserName, $desiredValue.UserName) + $returnValue = $false + } + + # Assume the string is our username when the matching desired value is actually a credential + if ($currentType.Name -eq 'string' -and $currentValue -eq $desiredValue.UserName) + { + Write-Verbose -Message ($script:localizedData.MatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + continue + } + else { - if ($DesiredValues.GetType().Name -eq 'HashTable' -or ` - $DesiredValues.GetType().Name -eq 'PSBoundParametersDictionary') + Write-Verbose -Message ($script:localizedData.NoMatchPsCredentialUsernameMessage -f $currentValue, $desiredValue.UserName) + $returnValue = $false + } + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) + { + Write-Verbose -Message ($script:localizedData.NoMatchTypeMismatchMessage -f $key, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue + } + } + + if ($currentValue -eq $desiredValue -and -not $desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredValuesClean.GetType().Name -in 'HashTable', 'PSBoundParametersDictionary') + { + $checkDesiredValue = $desiredValuesClean.ContainsKey($key) + } + else + { + $checkDesiredValue = Test-DscObjectHasProperty -Object $desiredValuesClean -PropertyName $key + } + + if (-not $checkDesiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + continue + } + + if ($desiredType.IsArray) + { + Write-Verbose -Message ($script:localizedData.TestDscParameterCompareMessage -f $key, $desiredType.FullName) + + if (-not $currentValue -and -not $desiredValue) + { + Write-Verbose -Message ($script:localizedData.MatchValueMessage -f $desiredType.FullName, $key, 'empty array', 'empty array') + continue + } + elseif (-not $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false + continue + } + elseif ($currentValue.Count -ne $desiredValue.Count) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueDifferentCountMessage -f $desiredType.FullName, $key, $currentValue.Count, $desiredValue.Count) + $returnValue = $false + continue + } + else + { + $desiredArrayValues = $desiredValue + $currentArrayValues = $currentValue + + if ($SortArrayValues) { - $checkDesiredValue = $DesiredValues.ContainsKey($_) + $desiredArrayValues = $desiredArrayValues | Sort-Object + $currentArrayValues = $currentArrayValues | Sort-Object } - else + + for ($i = 0; $i -lt $desiredArrayValues.Count; $i++) { - # If DesiredValue is a CimInstance. - $checkDesiredValue = $false - if (([System.Boolean]($DesiredValues.PSObject.Properties.Name -contains $_)) -eq $true) + if ($null -ne $desiredArrayValues[$i]) { - if ($null -ne $DesiredValues.$_) - { - $checkDesiredValue = $true + $desiredType = $desiredArrayValues[$i].GetType() + } + else + { + $desiredType = @{ + Name = 'Unknown' } } - } - if ($checkDesiredValue) - { - $desiredType = $DesiredValues.$_.GetType() - $fieldName = $_ - if ($desiredType.IsArray -eq $true) + if ($null -ne $currentArrayValues[$i]) { - if (($CurrentValues.ContainsKey($fieldName) -eq $false) ` - -or ($null -eq $CurrentValues.$fieldName)) - { - Write-Verbose -Message ($script:localizedData.PropertyValidationError -f $fieldName) -Verbose - - $returnValue = $false + $currentType = $currentArrayValues[$i].GetType() + } + else + { + $currentType = @{ + Name = 'Unknown' } - else + } + + if (-not $TurnOffTypeChecking) + { + if (($desiredType.Name -ne 'Unknown' -and $currentType.Name -ne 'Unknown') -and + $desiredType.FullName -ne $currentType.FullName) { - $arrayCompare = Compare-Object -ReferenceObject $CurrentValues.$fieldName ` - -DifferenceObject $DesiredValues.$fieldName - if ($null -ne $arrayCompare) - { - Write-Verbose -Message ($script:localizedData.PropertiesDoesNotMatch -f $fieldName) -Verbose - - $arrayCompare | ForEach-Object -Process { - Write-Verbose -Message ($script:localizedData.PropertyThatDoesNotMatch -f $_.InputObject, $_.SideIndicator) -Verbose - } - - $returnValue = $false - } + Write-Verbose -Message ($script:localizedData.NoMatchElementTypeMismatchMessage -f $key, $i, $currentType.FullName, $desiredType.FullName) + $returnValue = $false + continue } } + + if ($desiredArrayValues[$i] -ne $currentArrayValues[$i]) + { + Write-Verbose -Message ($script:localizedData.NoMatchElementValueMismatchMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + $returnValue = $false + continue + } else { - switch ($desiredType.Name) - { - 'String' - { - if (-not [System.String]::IsNullOrEmpty($CurrentValues.$fieldName) -or ` - -not [System.String]::IsNullOrEmpty($DesiredValues.$fieldName)) - { - Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` - -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose - - $returnValue = $false - } - } - - 'Int32' - { - if (-not ($DesiredValues.$fieldName -eq 0) -or ` - -not ($null -eq $CurrentValues.$fieldName)) - { - Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` - -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose - - $returnValue = $false - } - } - - { $_ -eq 'Int16' -or $_ -eq 'UInt16' -or $_ -eq 'Single' } - { - if (-not ($DesiredValues.$fieldName -eq 0) -or ` - -not ($null -eq $CurrentValues.$fieldName)) - { - Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` - -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose - - $returnValue = $false - } - } - - 'Boolean' - { - if ($CurrentValues.$fieldName -ne $DesiredValues.$fieldName) - { - Write-Verbose -Message ($script:localizedData.ValueOfTypeDoesNotMatch ` - -f $desiredType.Name, $fieldName, $($CurrentValues.$fieldName), $($DesiredValues.$fieldName)) -Verbose - - $returnValue = $false - } - } - - default - { - Write-Warning -Message ($script:localizedData.UnableToCompareProperty ` - -f $fieldName, $desiredType.Name) - - $returnValue = $false - } - } + Write-Verbose -Message ($script:localizedData.MatchElementValueMessage -f $i, $desiredType.FullName, $key, $currentArrayValues[$i], $desiredArrayValues[$i]) + continue } } + + } + } + elseif ($desiredType -eq [System.Collections.Hashtable] -and $currentType -eq [System.Collections.Hashtable]) + { + $param = $PSBoundParameters + $param.CurrentValues = $currentValue + $param.DesiredValues = $desiredValue + $null = $param.Remove('ValuesToCheck') + + if ($returnValue) + { + $returnValue = Test-DscParameterState @param + } + else + { + Test-DscParameterState @param | Out-Null + } + continue + } + else + { + if ($desiredValue -ne $currentValue) + { + Write-Verbose -Message ($script:localizedData.NoMatchValueMessage -f $desiredType.FullName, $key, $currentValue, $desiredValue) + $returnValue = $false } } } + if ($ReverseCheck) + { + Write-Verbose -Message $script:localizedData.StartingReverseCheck + $reverseCheckParameters = $PSBoundParameters + $reverseCheckParameters.CurrentValues = $DesiredValues + $reverseCheckParameters.DesiredValues = $CurrentValues + $null = $reverseCheckParameters.Remove('ReverseCheck') + + if ($returnValue) + { + $returnValue = Test-DscParameterState @reverseCheckParameters + } + else + { + $null = Test-DscParameterState @reverseCheckParameters + } + } + + Write-Verbose -Message ($script:localizedData.TestDscParameterResultMessage -f $returnValue) + return $returnValue } diff --git a/source/Public/Test-IsNanoServer.ps1 b/source/Public/Test-IsNanoServer.ps1 index 619cf4c..1efcc83 100644 --- a/source/Public/Test-IsNanoServer.ps1 +++ b/source/Public/Test-IsNanoServer.ps1 @@ -1,6 +1,15 @@ <# .SYNOPSIS Tests if the current OS is a Nano server. + + .DESCRIPTION + Tests if the current OS is a Nano server. + + .EXAMPLE + Test-IsNanoServer + + Returns $true if the current operating system is Nano Server, if not $false + is returned. #> function Test-IsNanoServer { diff --git a/source/en-US/DscResource.Common.psd1 b/source/en-US/DscResource.Common.psd1 deleted file mode 100644 index 7059207..0000000 --- a/source/en-US/DscResource.Common.psd1 +++ /dev/null @@ -1,17 +0,0 @@ -# Localized English (en-US) strings. - -ConvertFrom-StringData @' - PropertyTypeInvalidForDesiredValues = Property 'DesiredValues' must be either a [System.Collections.Hashtable], [CimInstance] or [PSBoundParametersDictionary]. The type detected was {0}. (DRC0001) - PropertyTypeInvalidForValuesToCheck = If 'DesiredValues' is a CimInstance, then property 'ValuesToCheck' must contain a value. (DRC0002) - PropertyValidationError = Expected to find an array value for property {0} in the current values, but it was either not present or was null. This has caused the test method to return false. (DRC0003) - PropertiesDoesNotMatch = Found an array for property {0} in the current values, but this array does not match the desired state. Details of the changes are below. (DRC0004) - PropertyThatDoesNotMatch = {0} - {1} (DRC0005) - ValueOfTypeDoesNotMatch = {0} value for property {1} does not match. Current state is '{2}' and desired state is '{3}'. (DRC0006) - UnableToCompareProperty = Unable to compare property {0} as the type {1} is not handled by the Test-DscParameterState cmdlet. (DRC0007) - TestIsNanoServerOperatingSystemSku = OperatingSystemSKU {0} was returned by Win32_OperatingSystem when detecting if operating system is Nano Server. (DRC0008) - ModuleNotFound = Please ensure that the PowerShell module '{0}' is installed. (DRC0009) - ParameterUsageWrong = None of the parameter(s) '{0}' may be used at the same time as any of the parameter(s) '{1}'. (DRC0010) - AddressFormatError = Address '{0}' is not in the correct format. Please correct the Address parameter in the configuration and try again. (DRC0011) - AddressIPv4MismatchError = Address '{0}' is in IPv4 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0012) - AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) -'@ diff --git a/source/en-US/DscResource.Common.strings.psd1 b/source/en-US/DscResource.Common.strings.psd1 new file mode 100644 index 0000000..4662c4c --- /dev/null +++ b/source/en-US/DscResource.Common.strings.psd1 @@ -0,0 +1,25 @@ +# Localized English (en-US) strings. + +ConvertFrom-StringData @' + TestIsNanoServerOperatingSystemSku = OperatingSystemSKU {0} was returned by Win32_OperatingSystem when detecting if operating system is Nano Server. (DRC0008) + ModuleNotFound = Please ensure that the PowerShell module '{0}' is installed. (DRC0009) + ParameterUsageWrong = None of the parameter(s) '{0}' may be used at the same time as any of the parameter(s) '{1}'. (DRC0010) + AddressFormatError = Address '{0}' is not in the correct format. Please correct the Address parameter in the configuration and try again. (DRC0011) + AddressIPv4MismatchError = Address '{0}' is in IPv4 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0012) + AddressIPv6MismatchError = Address '{0}' is in IPv6 format, which does not match server address family {1}. Please correct either of them in the configuration and try again. (DRC0013) + InvalidDesiredValuesError = Property 'DesiredValues' in Test-DscParameterState must be either a Hashtable or CimInstance. Type detected was '{0}'. (DRC0014) + InvalidCurrentValuesError = Property 'CurrentValues' in Test-DscParameterState must be either a Hashtable, CimInstance, or CimIntance[]. Type detected was '{0}'. (DRC0015) + InvalidValuesToCheckError = If 'DesiredValues' is a CimInstance then property 'ValuesToCheck' must contain a value. (DRC0016) + MatchPsCredentialUsernameMessage = MATCH: PSCredential username match. Current state is '{0}' and desired state is '{1}'. (DRC0017) + NoMatchPsCredentialUsernameMessage = NOTMATCH: PSCredential username mismatch. Current state is '{0}' and desired state is '{1}'. (DRC0018) + NoMatchTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type is '{1}' and desired type is '{2}'. (DRC0019) + MatchValueMessage = MATCH: Value (type '{0}') for property '{1}' does match. Current state is '{2}' and desired state is '{3}'. (DRC0020) + NoMatchValueMessage = NOTMATCH: Value (type '{0}') for property '{1}' does not match. Current state is '{2}' and desired state is '{3}'. (DRC0021) + NoMatchValueDifferentCountMessage = NOTMATCH: Value (type '{0}') for property '{1}' does have a different count. Current state count is '{2}' and desired state count is '{3}'. (DRC0022) + NoMatchElementTypeMismatchMessage = NOTMATCH: Type mismatch for property '{0}' Current state type of element [{1}] is '{2}' and desired type is '{3}'. (DRC0023) + NoMatchElementValueMismatchMessage = NOTMATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0024) + MatchElementValueMessage = MATCH: Value [{0}] (type '{1}') for property '{2}' does match. Current state is '{3}' and desired state is '{4}'. (DRC0025) + TestDscParameterResultMessage = Test-DscParameter result is '{0}'. (DRC0026) + StartingReverseCheck = Starting with a reverse check. (DRC0027) + TestDscParameterCompareMessage = Comparing values in property '{0}'. (DRC0028) +'@ diff --git a/source/prefix.ps1 b/source/prefix.ps1 index 91610ce..beeeaff 100644 --- a/source/prefix.ps1 +++ b/source/prefix.ps1 @@ -1 +1 @@ -$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent \ No newline at end of file +$script:modulesFolderPath = Split-Path -Path $PSScriptRoot -Parent diff --git a/source/suffix.ps1 b/source/suffix.ps1 index 1e275d9..d9829f8 100644 --- a/source/suffix.ps1 +++ b/source/suffix.ps1 @@ -1,2 +1 @@ - $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' diff --git a/tests/Unit/Public/ConvertTo-CimInstance.Tests.ps1 b/tests/Unit/Public/ConvertTo-CimInstance.Tests.ps1 new file mode 100644 index 0000000..a31e4f9 --- /dev/null +++ b/tests/Unit/Public/ConvertTo-CimInstance.Tests.ps1 @@ -0,0 +1,60 @@ +# macOS and Linux does not support CimInstance. +if (-not ($isWindows -or $PSEdition -eq 'Desktop')) +{ + return +} + +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest $_.FullName -ErrorAction Stop + } + catch + { + $false + } ) + }).BaseName + +Import-Module $ProjectName -Force + +InModuleScope $ProjectName { + Describe 'NetworkingDsc.Common\ConvertTo-CimInstance' { + $hashtable = @{ + k1 = 'v1' + k2 = 100 + k3 = 1, 2, 3 + } + + Context 'When the array contains the expected record count' { + It 'Should not throw exception' { + { + $script:result = [Microsoft.Management.Infrastructure.CimInstance[]] ( + $hashtable | ConvertTo-CimInstance + ) + } | Should -Not -Throw + } + + It "Should record count should be $($hashTable.Count)" { + $script:result.Count | Should -Be $hashtable.Count + } + + It 'Should return result of type CimInstance[]' { + $script:result.GetType().Name | Should -Be 'CimInstance[]' + } + + It 'Should return value "k1" in the CimInstance array should be "v1"' { + ($script:result | Where-Object Key -eq k1).Value | Should -Be 'v1' + } + + It 'Should return value "k2" in the CimInstance array should be "100"' { + ($script:result | Where-Object Key -eq k2).Value | Should -Be 100 + } + + It 'Should return value "k3" in the CimInstance array should be "1,2,3"' { + ($script:result | Where-Object Key -eq k3).Value | Should -Be '1,2,3' + } + } + } +} diff --git a/tests/Unit/Public/ConvertTo-Hashtable.Tests.ps1 b/tests/Unit/Public/ConvertTo-Hashtable.Tests.ps1 new file mode 100644 index 0000000..c353703 --- /dev/null +++ b/tests/Unit/Public/ConvertTo-Hashtable.Tests.ps1 @@ -0,0 +1,56 @@ +# macOS and Linux does not support CimInstance. +if (-not ($isWindows -or $PSEdition -eq 'Desktop')) +{ + return +} + +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest $_.FullName -ErrorAction Stop + } + catch + { + $false + } ) + }).BaseName + +Import-Module $ProjectName -Force + +InModuleScope $ProjectName { + Describe 'NetworkingDsc.Common\ConvertTo-HashTable' { + [Microsoft.Management.Infrastructure.CimInstance[]] $cimInstances = ConvertTo-CimInstance -Hashtable @{ + k1 = 'v1' + k2 = 100 + k3 = 1, 2, 3 + } + + Context 'When the array contains the expected record count' { + It 'Should not throw exception' { + { $script:result = $cimInstances | ConvertTo-HashTable } | Should -Not -Throw + } + + It "Should return record count of $($cimInstances.Count)" { + $script:result.Count | Should -Be $cimInstances.Count + } + + It 'Should return result of type [System.Collections.Hashtable]' { + $script:result | Should -BeOfType [System.Collections.Hashtable] + } + + It 'Should return value "k1" in the hashtable should be "v1"' { + $script:result.k1 | Should -Be 'v1' + } + + It 'Should return value "k2" in the hashtable should be "100"' { + $script:result.k2 | Should -Be 100 + } + + It 'Should return value "k3" in the hashtable should be "1,2,3"' { + $script:result.k3 | Should -Be '1,2,3' + } + } + } +} diff --git a/tests/Unit/Public/Remove-CommonParameter.Tests.ps1 b/tests/Unit/Public/Remove-CommonParameter.Tests.ps1 new file mode 100644 index 0000000..d53228b --- /dev/null +++ b/tests/Unit/Public/Remove-CommonParameter.Tests.ps1 @@ -0,0 +1,67 @@ +$ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path +$ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ + ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and + $(try + { + Test-ModuleManifest $_.FullName -ErrorAction Stop + } + catch + { + $false + } ) + }).BaseName + +Import-Module $ProjectName -Force + +InModuleScope $ProjectName { + Describe 'NetworkingDsc.Common\Remove-CommonParameter' { + $removeCommonParameter = @{ + Parameter1 = 'value1' + Parameter2 = 'value2' + Verbose = $true + Debug = $true + ErrorAction = 'Stop' + WarningAction = 'Stop' + InformationAction = 'Stop' + ErrorVariable = 'errorVariable' + WarningVariable = 'warningVariable' + OutVariable = 'outVariable' + OutBuffer = 'outBuffer' + PipelineVariable = 'pipelineVariable' + InformationVariable = 'informationVariable' + WhatIf = $true + Confirm = $true + UseTransaction = $true + } + + Context 'Hashtable contains all common parameters' { + It 'Should not throw exception' { + { + $script:result = Remove-CommonParameter -Hashtable $removeCommonParameter -Verbose + } | Should -Not -Throw + } + + It 'Should have retained parameters in the hashtable' { + $script:result.Contains('Parameter1') | Should -BeTrue + $script:result.Contains('Parameter2') | Should -BeTrue + } + + It 'Should have removed the common parameters from the hashtable' { + $script:result.Contains('Verbose') | Should -BeFalse + $script:result.Contains('Debug') | Should -BeFalse + $script:result.Contains('ErrorAction') | Should -BeFalse + $script:result.Contains('WarningAction') | Should -BeFalse + $script:result.Contains('InformationAction') | Should -BeFalse + $script:result.Contains('ErrorVariable') | Should -BeFalse + $script:result.Contains('WarningVariable') | Should -BeFalse + $script:result.Contains('OutVariable') | Should -BeFalse + $script:result.Contains('OutBuffer') | Should -BeFalse + $script:result.Contains('PipelineVariable') | Should -BeFalse + $script:result.Contains('InformationVariable') | Should -BeFalse + $script:result.Contains('WhatIf') | Should -BeFalse + $script:result.Contains('Confirm') | Should -BeFalse + $script:result.Contains('UseTransaction') | Should -BeFalse + } + } + } +} diff --git a/tests/Unit/Public/Test-DscParameterState.Tests.ps1 b/tests/Unit/Public/Test-DscParameterState.Tests.ps1 index c8f641c..7c7394a 100644 --- a/tests/Unit/Public/Test-DscParameterState.Tests.ps1 +++ b/tests/Unit/Public/Test-DscParameterState.Tests.ps1 @@ -1,401 +1,932 @@ $ProjectPath = "$PSScriptRoot\..\..\.." | Convert-Path $ProjectName = ((Get-ChildItem -Path $ProjectPath\*\*.psd1).Where{ ($_.Directory.Name -match 'source|src' -or $_.Directory.Name -eq $_.BaseName) -and - $(try { Test-ModuleManifest $_.FullName -ErrorAction Stop } catch { $false } ) + $(try + { + Test-ModuleManifest $_.FullName -ErrorAction Stop + } + catch + { + $false + } ) }).BaseName Import-Module $ProjectName -Force InModuleScope $ProjectName { - Describe 'Test-DscParameterState' -Tag 'TestDscParameterState' { - Context 'When passing values' { - It 'Should return true for two identical tables' { - $mockDesiredValues = @{ - Example = 'test' + Describe 'ComputerManagementDsc.Common\Test-DscParameterState' { + $verbose = $true + + Context 'When testing single values' { + $currentValues = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' } + } - $testParameters = @{ - CurrentValues = $mockDesiredValues - DesiredValues = $mockDesiredValues + Context 'When all values match' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } } - Test-DscParameterState @testParameters | Should -Be $true - } + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } - It 'Should return false when a value is different for [System.String]' { - $mockCurrentValues = @{ - Example = [System.String] 'something' + It 'Should return $true' { + $script:result | Should -BeTrue } + } - $mockDesiredValues = @{ - Example = [System.String] 'test' + Context 'When a string is mismatched' { + $desiredValues = [PSObject] @{ + String = 'different string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return false when a value is different for [System.Int32]' { - $mockCurrentValues = @{ - Example = [System.Int32] 1 + Context 'When a boolean is mismatched' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $false + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } } - $mockDesiredValues = @{ - Example = [System.Int32] 2 + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should return $false' { + $script:result | Should -BeFalse } - - Test-DscParameterState @testParameters | Should -Be $false } - It 'Should return false when a value is different for [Int16]' { - $mockCurrentValues = @{ - Example = [System.Int16] 1 + Context 'When an int is mismatched' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse } + } - $mockDesiredValues = @{ - Example = [System.Int16] 2 + Context 'When a type is mismatched' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = '99' + Array = 'a', 'b', 'c' } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return false when a value is different for [UInt16]' { - $mockCurrentValues = @{ - Example = [System.UInt16] 1 + Context 'When a type is mismatched but TurnOffTypeChecking is used' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = '99' + Array = 'a', 'b', 'c' + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -TurnOffTypeChecking ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $true' { + $script:result | Should -BeTrue } + } - $mockDesiredValues = @{ - Example = [System.UInt16] 2 + Context 'When a value is mismatched but valuesToCheck is used to exclude them' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $false + Int = 1 + Array = @( 'a', 'b' ) } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + $valuesToCheck = @( + 'String' + ) + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -ValuesToCheck $valuesToCheck ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $true' { + $script:result | Should -BeTrue + } } + } - It 'Should return false when a value is different for [Boolean]' { - $mockCurrentValues = @{ - Example = [System.Boolean] $true + Context 'When testing array values' { + BeforeAll { + $currentValues = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c', 1 + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } } + } - $mockDesiredValues = @{ - Example = [System.Boolean] $false + Context 'When array is missing a value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return false when a value is missing' { - $mockCurrentValues = @{} - $mockDesiredValues = @{ - Example = 'test' + Context 'When array has an additional value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'a', 'b', 'c', 1, 2 } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return true when only a specified value matches, but other non-listed values do not' { - $mockCurrentValues = @{ - Example = 'test' - SecondExample = 'true' + Context 'When array has a different value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'a', 'x', 'c', 1 + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse } + } - $mockDesiredValues = @{ - Example = 'test' - SecondExample = 'false' + Context 'When array has different order' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'c', 'b', 'a', 1 } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues - ValuesToCheck = @('Example') + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $true + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return false when only specified values do not match, but other non-listed values do ' { - $mockCurrentValues = @{ - Example = 'test' - SecondExample = 'true' + Context 'When array has different order but SortArrayValues is used' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 1 + Array = 'c', 'b', 'a', 1 + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -SortArrayValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse } + } - $mockDesiredValues = @{ - Example = 'test' - SecondExample = 'false' + + Context 'When array has a value with a different type' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c', '1' } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues - ValuesToCheck = @('SecondExample') + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return false when an empty hash table is used in the current values' { - $mockCurrentValues = @{} - $mockDesiredValues = @{ - Example = 'test' - SecondExample = 'false' + Context 'When array has a value with a different type but TurnOffTypeChecking is used' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c', '1' } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -TurnOffTypeChecking ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $true' { + $script:result | Should -BeTrue + } } - It 'Should return true when evaluating a table against a CimInstance' -skip:(!($isWindows -or $PSEdition -eq 'Desktop')) { - $mockCurrentValues = @{ - Handle = '0' - ProcessId = '1000' + Context 'When both arrays are empty' { + $currentValues = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = @() + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = @() + } } - $mockWin32ProcessProperties = @{ - Handle = 0 - ProcessId = 1000 + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = @() + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = @() + } } - $mockNewCimInstanceParameters = @{ - ClassName = 'Win32_Process' - Property = $mockWin32ProcessProperties - Key = 'Handle' - ClientOnly = $true + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - $mockDesiredValues = New-CimInstance @mockNewCimInstanceParameters + It 'Should return $true' { + $script:result | Should -BeTrue + } + } + } + + Context 'When testing hashtables' { + $currentValues = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + } + + Context 'When hashtable is missing a value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } + } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues - ValuesToCheck = @('Handle','ProcessId') + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $true + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return false when evaluating a table against a CimInstance and a value is wrong' -skip:(!($isWindows -or $PSEdition -eq 'Desktop')) { - $mockCurrentValues = @{ - Handle = '1' - ProcessId = '1000' + Context 'When hashtable has an additional value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99, 100 + } } - $mockWin32ProcessProperties = @{ - Handle = 0 - ProcessId = 1000 + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - $mockNewCimInstanceParameters = @{ - ClassName = 'Win32_Process' - Property = $mockWin32ProcessProperties - Key = 'Handle' - ClientOnly = $true + It 'Should return $false' { + $script:result | Should -BeFalse } + } - $mockDesiredValues = New-CimInstance @mockNewCimInstanceParameters + Context 'When hashtable has a different value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'xx', 'v2', 'v3', 99 + } + } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues - ValuesToCheck = @('Handle','ProcessId') + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } - It 'Should return true when evaluating a hash table containing an array' { - $mockCurrentValues = @{ - Example = 'test' - SecondExample = @('1','2') + Context 'When an array in hashtable has different order' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v3', 'v2', 'v1', 99 + } } - $mockDesiredValues = @{ - Example = 'test' - SecondExample = @('1','2') + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should return $false' { + $script:result | Should -BeFalse + } + } + + Context 'When an array in hashtable has different order but SortArrayValues is used' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v3', 'v2', 'v1', 99 + } } - Test-DscParameterState @testParameters | Should -Be $true + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -SortArrayValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $true' { + $script:result | Should -BeTrue + } } - It 'Should return false when evaluating a hash table containing an array with wrong values' { - $mockCurrentValues = @{ - Example = 'test' - SecondExample = @('A','B') + + Context 'When hashtable has a value with a different type' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', '99' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse } + } - $mockDesiredValues = @{ - Example = 'test' - SecondExample = @('1','2') + Context 'When hashtable has a value with a different type but TurnOffTypeChecking is used' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -TurnOffTypeChecking ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $true' { + $script:result | Should -BeTrue + } } + } - It 'Should return false when evaluating a hash table containing an array, but the CurrentValues are missing an array' { - $mockCurrentValues = @{ - Example = 'test' + Context 'When reverse checking' { + $currentValues = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c', 1 + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' } + } - $mockDesiredValues = @{ - Example = 'test' - SecondExample = @('1','2') + Context 'When even if missing property in the desired state' { + $desiredValues = [PSObject] @{ + Array = 'a', 'b', 'c', 1 + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3' + } } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $true' { + $script:result | Should -BeTrue + } } - It 'Should return false when evaluating a hash table containing an array, but the property i CurrentValues is $null' { - $mockCurrentValues = @{ - Example = 'test' - SecondExample = $null + Context 'When missing property in the desired state' { + $currentValues = @{ + String = 'a string' + Bool = $true } - $mockDesiredValues = @{ - Example = 'test' - SecondExample = @('1','2') + $desiredValues = [PSObject] @{ + String = 'a string' } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -ReverseCheck ` + -Verbose:$verbose } | Should -Not -Throw } - Test-DscParameterState @testParameters | Should -Be $false + It 'Should return $false' { + $script:result | Should -BeFalse + } } } - Context 'When passing invalid types for DesiredValues' { - It 'Should throw the correct error when DesiredValues is of wrong type' { - $mockCurrentValues = @{ - Example = 'something' + Context 'When testing parameter types' { + Context 'When desired value is of the wrong type' { + $currentValues = @{ + String = 'a string' } - $mockDesiredValues = 'NotHashTable' + $desiredValues = 1, 2, 3 - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Throw } + } - $mockCorrectErrorMessage = ($script:localizedData.PropertyTypeInvalidForDesiredValues -f $testParameters.DesiredValues.GetType().Name) - { Test-DscParameterState @testParameters } | Should -Throw $mockCorrectErrorMessage + Context 'When current value is of the wrong type' { + $currentValues = 1, 2, 3 + + $desiredValues = @{ + String = 'a string' + } + + It 'Should throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Throw + } } + } - It 'Should write a warning when DesiredValues contain an unsupported type' { - Mock -CommandName Write-Warning -Verifiable + # macOS and Linux does not support CimInstance. + if ($isWindows -or $PSEdition -eq 'Desktop') + { + Context 'When testing CimInstances / hashtables' { + $currentValues = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } - # This is a dummy type to test with a type that could never be a correct one. - class MockUnknownType - { - [ValidateNotNullOrEmpty()] - [System.String] - $Property1 + CimInstances = [Microsoft.Management.Infrastructure.CimInstance[]] ( + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + ) + } + + Context 'When everything matches' { + $desiredValues = [PSObject]@{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + + CimInstances = [Microsoft.Management.Infrastructure.CimInstance[]] ( + ConvertTo-CimInstance -Hashtable @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + ) + } - [ValidateNotNullOrEmpty()] - [System.String] - $Property2 + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } - MockUnknownType() - { + It 'Should return $true' { + $script:result | Should -BeTrue } } - $mockCurrentValues = @{ - Example = New-Object -TypeName MockUnknownType - } + Context 'When CimInstances missing a value in the desired state (not recognized)' { + $desiredValues = [PSObject]@{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + CimInstances = @{ + String = 'a string' + Bool = $true + Array = 'a, b, c' + } + } - $mockDesiredValues = @{ - Example = New-Object -TypeName MockUnknownType - } + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues + It 'Should return $true' { + $script:result | Should -BeTrue + } } - Test-DscParameterState @testParameters | Should -Be $false + Context 'When CimInstances missing a value in the desired state (recognized using ReverseCheck)' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + CimInstances = @{ + String = 'a string' + Bool = $true + Array = 'a, b, c' + } + } - Assert-MockCalled -CommandName Write-Warning -Exactly -Times 1 - } - } + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -ReverseCheck ` + -Verbose:$verbose } | Should -Not -Throw + } - Context 'When passing an CimInstance as DesiredValue and ValuesToCheck is $null' { - It 'Should throw the correct error' -skip:(!($isWindows -or $PSEdition -eq 'Desktop')) { - $mockCurrentValues = @{ - Example = 'something' + It 'Should return $false' { + $script:result | Should -BeFalse + } } - $mockWin32ProcessProperties = @{ - Handle = 0 - ProcessId = 1000 + Context 'When CimInstances have an additional value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + CimInstances = @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a, b, c' + Test = 'Some string' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse + } } - $mockNewCimInstanceParameters = @{ - ClassName = 'Win32_Process' - Property = $mockWin32ProcessProperties - Key = 'Handle' - ClientOnly = $true + Context 'When CimInstances have a different value' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + CimInstances = @{ + String = 'some other string' + Bool = $true + Int = 99 + Array = 'a, b, c' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $false' { + $script:result | Should -BeFalse + } } - $mockDesiredValues = New-CimInstance @mockNewCimInstanceParameters + Context 'When CimInstances have a value with a different type' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + CimInstances = @{ + String = 'a string' + Bool = $true + Int = '99' + Array = 'a, b, c' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -Verbose:$verbose } | Should -Not -Throw + } - $testParameters = @{ - CurrentValues = $mockCurrentValues - DesiredValues = $mockDesiredValues - ValuesToCheck = $null + It 'Should return $false' { + $script:result | Should -BeFalse + } } - $mockCorrectErrorMessage = $script:localizedData.PropertyTypeInvalidForValuesToCheck - { Test-DscParameterState @testParameters } | Should -Throw $mockCorrectErrorMessage + Context 'When CimInstances have a value with a different type but TurnOffTypeChecking is used' { + $desiredValues = [PSObject] @{ + String = 'a string' + Bool = $true + Int = 99 + Array = 'a', 'b', 'c' + Hashtable = @{ + k1 = 'Test' + k2 = 123 + k3 = 'v1', 'v2', 'v3', 99 + } + CimInstances = @{ + String = 'a string' + Bool = $true + Int = '99' + Array = 'a, b, c' + } + } + + It 'Should not throw exception' { + { $script:result = Test-DscParameterState ` + -CurrentValues $currentValues ` + -DesiredValues $desiredValues ` + -TurnOffTypeChecking ` + -Verbose:$verbose } | Should -Not -Throw + } + + It 'Should return $true' { + $script:result | Should -BeTrue + } + } } } - - Assert-VerifiableMock } }