From fcff2e040bf8162f8138eb08612fde3e3c60ef42 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Fri, 10 Mar 2023 02:38:01 +0100 Subject: [PATCH] PSResourceRepository: Add property `Reasons` (#402) * PSResourceRepository: Fix property `Reasons` * Fix/reasons psrepository integ test and unit tests (#3) * Fix unit test for PowerShell and Windows PowerShell * Remove Reasons class * Fix correct type for Reasons * Fix review comments * Fix correct type * Fix integration test * Fix review comment --- CHANGELOG.md | 4 + source/Classes/001.CMReason.ps1 | 21 +++++ source/Classes/020.PSResourceRepository.ps1 | 15 +++- ...PSResourceRepository.integration.tests.ps1 | 73 +++++++++++++++++ tests/Unit/Classes/CMReason.Tests.ps1 | 81 +++++++++++++++++++ .../Classes/PSResourceRepository.Tests.ps1 | 76 +++++++++++++++-- 6 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 source/Classes/001.CMReason.ps1 create mode 100644 tests/Unit/Classes/CMReason.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index db98c0a8..70b32d0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The class-based resources are now re-using the module DscResource.Base - Fixes [Issue #404](https://github.com/dsccommunity/ComputerManagementDsc/issues/404). - Removed the file `source/build.psd1` as it is no longer required for the build pipeline. +- PSResourceRepository + - The resource now supports the read-only property `Reasons` that the + compliance part (audit via Azure Policy) of Azure AutoManage Machine + Configuration uses. ## [9.0.0] - 2023-02-22 diff --git a/source/Classes/001.CMReason.ps1 b/source/Classes/001.CMReason.ps1 new file mode 100644 index 00000000..d5d70f9b --- /dev/null +++ b/source/Classes/001.CMReason.ps1 @@ -0,0 +1,21 @@ +<# + .SYNOPSIS + The reason a property of a DSC resource is not in desired state. + + .DESCRIPTION + A DSC resource can have a read-only property `Reasons` that the compliance + part (audit via Azure Policy) of Azure AutoManage Machine Configuration + uses. The property Reasons holds an array of CMReason. Each CMReason + explains why a property of a DSC resource is not in desired state. +#> + +class CMReason +{ + [DscProperty()] + [System.String] + $Code + + [DscProperty()] + [System.String] + $Phrase +} diff --git a/source/Classes/020.PSResourceRepository.ps1 b/source/Classes/020.PSResourceRepository.ps1 index fd620206..8b010f6f 100644 --- a/source/Classes/020.PSResourceRepository.ps1 +++ b/source/Classes/020.PSResourceRepository.ps1 @@ -48,6 +48,9 @@ and PackageManagementProvider may not be used in conjunction with Default. When the Default parameter is used, properties are not enforced when PSGallery properties are changed outside of Dsc. + .PARAMETER Reasons + Returns the reason a property is not in desired state. + .EXAMPLE Invoke-DscResource -ModuleName ComputerManagementDsc -Name PSResourceRepository -Method Get -Property @{ Name = 'PSTestRepository' @@ -96,7 +99,7 @@ class PSResourceRepository : ResourceBase $Proxy [DscProperty()] - [pscredential] + [PSCredential] $ProxyCredential [DscProperty()] @@ -112,6 +115,10 @@ class PSResourceRepository : ResourceBase [Nullable[System.Boolean]] $Default + [DscProperty(NotConfigurable)] + [CMReason[]] + $Reasons + # Passing the module's base directory to the base constructor so it finds localization files. PSResourceRepository () : base ($PSScriptRoot) { @@ -124,12 +131,12 @@ class PSResourceRepository : ResourceBase [PSResourceRepository] Get() { - return ([ResourceBase]$this).Get() + return ([ResourceBase] $this).Get() } [void] Set() { - ([ResourceBase]$this).Set() + ([ResourceBase] $this).Set() } [Boolean] Test() @@ -226,7 +233,7 @@ class PSResourceRepository : ResourceBase $returnValue.PublishLocation = $repository.PublishLocation $returnValue.ScriptPublishLocation = $repository.ScriptPublishLocation $returnValue.Proxy = $repository.Proxy - $returnValue.ProxyCredential = $repository.ProxyCredental + $returnValue.ProxyCredential = $repository.ProxyCredential $returnValue.InstallationPolicy = $repository.InstallationPolicy $returnValue.PackageManagementProvider = $repository.PackageManagementProvider } diff --git a/tests/Integration/Classes/PSResourceRepository.integration.tests.ps1 b/tests/Integration/Classes/PSResourceRepository.integration.tests.ps1 index 2a9e650b..41f93453 100644 --- a/tests/Integration/Classes/PSResourceRepository.integration.tests.ps1 +++ b/tests/Integration/Classes/PSResourceRepository.integration.tests.ps1 @@ -88,6 +88,8 @@ try $resourceCurrentState.PackageManagementProvider | Should -Be 'NuGet' $resourceCurrentState.InstallationPolicy | Should -Be 'Untrusted' + # Read-only properties + $resourceCurrentState.Reasons | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { @@ -152,6 +154,9 @@ try # Defaulted properties $resourceCurrentState.Ensure | Should -Be $shouldBeData.Ensure + + # Read-only properties + $resourceCurrentState.Reasons | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { @@ -218,6 +223,9 @@ try # Ensure will be Absent $resourceCurrentState.Ensure | Should -Be 'Absent' + + # Read-only properties + $resourceCurrentState.Reasons | Should -BeNullOrEmpty } It 'Should return $true when Test-DscConfiguration is run' { @@ -226,7 +234,72 @@ try } Wait-ForIdleLcm -Clear + } + + Context 'When using Invoke-DscResource' { + BeforeAll { + $script:mockInvokeDscResourceParameters = @{ + ModuleName = $script:dscModuleName + Name = $script:dscResourceFriendlyName + Verbose = $true + } + } + + BeforeEach { + Wait-ForIdleLcm -Clear + } + Context 'When the configuration is not in desired state' { + Context 'When calling method Get()' { + It 'Should not throw and return the correct values for each property' { + { + $script:resourceCurrentState = Invoke-DscResource @mockInvokeDscResourceParameters -Method 'Get' -Property @{ + Name = 'PSTestGallery' + Ensure = 'Present' + SourceLocation = 'https://www.nuget.org/api/v2' + PublishLocation = 'https://www.nuget.org/api/v2/package/' + ScriptSourceLocation = 'https://www.nuget.org/api/v2/items/psscript/' + ScriptPublishLocation = 'https://www.nuget.org/api/v2/package/' + InstallationPolicy = 'Trusted' + PackageManagementProvider = 'NuGet' + } + } | Should -Not -Throw + + $resourceCurrentState.Name | Should -Be 'PSTestGallery' + $resourceCurrentState.Ensure | Should -Be 'Absent' + $resourceCurrentState.PackageManagementProvider | Should -BeNullOrEmpty + $resourceCurrentState.Proxy | Should -BeNullOrEmpty + $resourceCurrentState.ProxyCredential | Should -BeNullOrEmpty + $resourceCurrentState.PublishLocation | Should -BeNullOrEmpty + $resourceCurrentState.ScriptPublishLocation | Should -BeNullOrEmpty + $resourceCurrentState.ScriptSourceLocation | Should -BeNullOrEmpty + $resourceCurrentState.SourceLocation | Should -BeNullOrEmpty + + $resourceCurrentState.Reasons | Should -HaveCount 7 + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:ScriptPublishLocation' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property ScriptPublishLocation should be "https://www.nuget.org/api/v2/package/", but was ""' + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:InstallationPolicy' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property InstallationPolicy should be "Trusted", but was ""' + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:Ensure' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property Ensure should be "Present", but was "Absent"' + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:PackageManagementProvider' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property PackageManagementProvider should be "NuGet", but was ""' + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:ScriptSourceLocation' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property ScriptSourceLocation should be "https://www.nuget.org/api/v2/items/psscript/", but was ""' + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:PublishLocation' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property PublishLocation should be "https://www.nuget.org/api/v2/package/", but was ""' + + $resourceCurrentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:SourceLocation' + $resourceCurrentState.Reasons.Phrase | Should -Contain 'The property SourceLocation should be "https://www.nuget.org/api/v2", but was ""' + } + } + } } #endregion } diff --git a/tests/Unit/Classes/CMReason.Tests.ps1 b/tests/Unit/Classes/CMReason.Tests.ps1 new file mode 100644 index 00000000..dbda4662 --- /dev/null +++ b/tests/Unit/Classes/CMReason.Tests.ps1 @@ -0,0 +1,81 @@ +<# + .SYNOPSIS + Unit test for PSResourceRepository DSC resource. +#> + +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +try +{ + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } +} +catch [System.IO.FileNotFoundException] +{ + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' +} + +try +{ + $script:dscModuleName = 'ComputerManagementDsc' + + Import-Module -Name $script:dscModuleName + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + + Describe 'CMReason' -Tag 'CMReason' { + Context 'When instantiating the class' { + It 'Should not throw an error' { + $script:mockCMReasonInstance = InModuleScope -ScriptBlock { + [CMReason]::new() + } + } + + It 'Should be of the correct type' { + $mockCMReasonInstance | Should -Not -BeNullOrEmpty + $mockCMReasonInstance.GetType().Name | Should -Be 'CMReason' + } + } + + Context 'When setting and reading values' { + It 'Should be able to set value in instance' { + $script:mockCMReasonInstance = InModuleScope -ScriptBlock { + $CMReasonInstance = [CMReason]::new() + + $CMReasonInstance.Code = 'SqlAudit:SqlAudit:Ensure' + $CMReasonInstance.Phrase = 'The property Ensure should be "Present", but was "Absent"' + + return $CMReasonInstance + } + } + + It 'Should be able read the values from instance' { + $mockCMReasonInstance.Code | Should -Be 'SqlAudit:SqlAudit:Ensure' + $mockCMReasonInstance.Phrase = 'The property Ensure should be "Present", but was "Absent"' + } + } + } +} +finally +{ + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} diff --git a/tests/Unit/Classes/PSResourceRepository.Tests.ps1 b/tests/Unit/Classes/PSResourceRepository.Tests.ps1 index 0bec3c3e..7dc06b17 100644 --- a/tests/Unit/Classes/PSResourceRepository.Tests.ps1 +++ b/tests/Unit/Classes/PSResourceRepository.Tests.ps1 @@ -82,7 +82,7 @@ try return [System.Collections.Hashtable] @{ Name = 'FakePSGallery' SourceLocation = 'https://www.powershellgallery.com/api/v2' - Ensure = 'Present' + Ensure = [Ensure]::Present InstallationPolicy = 'Untrusted' PackageManagementProvider = 'Nuget' } @@ -104,6 +104,7 @@ try $currentState.Default | Should -BeNullOrEmpty $currentState.InstallationPolicy | Should -Be 'Untrusted' $currentState.PackageManagementProvider | Should -Be 'NuGet' + $currentState.Reasons | Should -BeNullOrEmpty } } @@ -130,7 +131,7 @@ try ScriptPublishLocation = 'https://www.powershellgallery.com/api/v2/package/' InstallationPolicy = 'Untrusted' PackageManagementProvider = 'NuGet' - Ensure = 'Present' + Ensure = [Ensure]::Present } } -PassThru | Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { @@ -150,11 +151,12 @@ try $currentState.ProxyCredential | Should -BeNullOrEmpty $currentState.Credential | Should -BeNullOrEmpty $currentState.Default | Should -BeNullOrEmpty + $currentState.Reasons | Should -BeNullOrEmpty } } } - Context 'When the respository should be Absent' { + Context 'When the repository should be Absent' { It 'Should return the correct result when the Repository is Absent' { InModuleScope -ScriptBlock { $script:mockPSResourceRepositoryInstance = [PSResourceRepository] @{ @@ -165,7 +167,7 @@ try Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return [System.Collections.Hashtable] @{ Name = 'FakePSGallery' - Ensure = 'Absent' + Ensure = [Ensure]::Absent } } -PassThru | Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { @@ -183,6 +185,7 @@ try $currentState.ProxyCredential | Should -BeNullOrEmpty $currentState.Credential | Should -BeNullOrEmpty $currentState.Default | Should -BeNullOrEmpty + $currentState.Reasons | Should -BeNullOrEmpty } } } @@ -200,7 +203,7 @@ try Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return [System.Collections.Hashtable] @{ Name = 'FakePSGallery' - Ensure = 'Present' + Ensure = [Ensure]::Present SourceLocation = 'https://www.powershellgallery.com/api/v2' ScriptSourceLocation = 'https://www.powershellgallery.com/api/v2/items/psscript' PublishLocation = 'https://www.powershellgallery.com/api/v2/package/' @@ -222,6 +225,10 @@ try $currentState.ScriptPublishLocation | Should -Be 'https://www.powershellgallery.com/api/v2/package/' $currentState.InstallationPolicy | Should -Be 'Untrusted' $currentState.PackageManagementProvider | Should -Be 'NuGet' + + $currentState.Reasons | Should -HaveCount 1 + $currentState.Reasons[0].Code | Should -Be 'PSResourceRepository:PSResourceRepository:Ensure' + $currentState.Reasons[0].Phrase | Should -Be 'The property Ensure should be "Absent", but was "Present"' } } } @@ -238,7 +245,7 @@ try Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return [System.Collections.Hashtable] @{ Name = 'FakePSGallery' - Ensure = 'Absent' + Ensure = [Ensure]::Absent } } -PassThru | Add-Member -Force -MemberType 'ScriptMethod' -Name 'AssertProperties' -Value { @@ -254,6 +261,28 @@ try $currentState.ScriptPublishLocation | Should -BeNullOrEmpty $currentState.InstallationPolicy | Should -BeNullOrEmpty $currentState.PackageManagementProvider | Should -BeNullOrEmpty + + $currentState.Reasons | Should -HaveCount 2 + + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:SourceLocation' + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:Ensure' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:SourceLocation'}) + + # Handle PowerShell and Windows PowerShell differently. + if ($IsLinux -or $IsMacOS -or $IsWindows) + { + # PowerShell + $reason.Phrase | Should -Be 'The property SourceLocation should be "https://www.powershellgallery.com/api/v2", but was null' + } + else + { + # Windows PowerShell + $reason.Phrase | Should -Be 'The property SourceLocation should be "https://www.powershellgallery.com/api/v2", but was ""' + } + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:Ensure'}) + $reason.Phrase | Should -Be 'The property Ensure should be "Present", but was "Absent"' } } } @@ -275,7 +304,7 @@ try Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return [System.Collections.Hashtable] @{ Name = 'FakePSGallery' - Ensure = 'Present' + Ensure = [Ensure]::Present SourceLocation = 'https://www.notcorrect.com/api/v2' ScriptSourceLocation = 'https://www.notcorrect.com/api/v2/items/psscript' PublishLocation = 'https://www.notcorrect.com/api/v2/package/' @@ -297,6 +326,33 @@ try $currentState.ScriptPublishLocation | Should -Be 'https://www.notcorrect.com/api/v2/package/' $currentState.InstallationPolicy | Should -Be 'Trusted' $currentState.PackageManagementProvider | Should -Be 'Package' + + $currentState.Reasons | Should -HaveCount 6 + + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:SourceLocation' + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:InstallationPolicy' + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:ScriptSourceLocation' + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:ScriptPublishLocation' + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:PackageManagementProvider' + $currentState.Reasons.Code | Should -Contain 'PSResourceRepository:PSResourceRepository:PublishLocation' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:SourceLocation'}) + $reason.Phrase | Should -Be 'The property SourceLocation should be "https://www.powershellgallery.com/api/v2", but was "https://www.notcorrect.com/api/v2"' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:InstallationPolicy'}) + $reason.Phrase | Should -Be 'The property InstallationPolicy should be "Untrusted", but was "Trusted"' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:ScriptSourceLocation'}) + $reason.Phrase | Should -Be 'The property ScriptSourceLocation should be "https://www.powershellgallery.com/api/v2/items/psscript", but was "https://www.notcorrect.com/api/v2/items/psscript"' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:ScriptPublishLocation'}) + $reason.Phrase | Should -Be 'The property ScriptPublishLocation should be "https://www.powershellgallery.com/api/v2/package/", but was "https://www.notcorrect.com/api/v2/package/"' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:PackageManagementProvider'}) + $reason.Phrase | Should -Be 'The property PackageManagementProvider should be "NuGet", but was "Package"' + + $reason = $currentState.Reasons.Where({$_.Code -eq 'PSResourceRepository:PSResourceRepository:PublishLocation'}) + $reason.Phrase | Should -Be 'The property PublishLocation should be "https://www.powershellgallery.com/api/v2/package/", but was "https://www.notcorrect.com/api/v2/package/"' } } @@ -310,7 +366,7 @@ try Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetCurrentState' -Value { return [System.Collections.Hashtable] @{ Name = 'FakePSGallery' - Ensure = 'Present' + Ensure = [Ensure]::Present SourceLocation = 'https://www.notcorrect.com/api/v2' ScriptSourceLocation = 'https://www.notcorrect.com/api/v2/items/psscript' PublishLocation = 'https://www.notcorrect.com/api/v2/package/' @@ -332,6 +388,10 @@ try $currentState.ScriptPublishLocation | Should -Be 'https://www.notcorrect.com/api/v2/package/' $currentState.InstallationPolicy | Should -Be 'Trusted' $currentState.PackageManagementProvider | Should -Be 'Package' + + $currentState.Reasons | Should -HaveCount 1 + $currentState.Reasons[0].Code | Should -Be 'PSResourceRepository:PSResourceRepository:Ensure' + $currentState.Reasons[0].Phrase | Should -Be 'The property Ensure should be "Absent", but was "Present"' } } }