diff --git a/CHANGELOG.md b/CHANGELOG.md index f39c6d78..a5473de5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- PSResource + - New class-based resource to manage PowerShell resources. - Fixes [Issue #393](https://github.com/dsccommunity/ComputerManagementDsc/issues/393), [Issue #398](https://github.com/dsccommunity/ComputerManagementDsc/issues/398) - PSResourceRepository - New class-based resource to manage PowerShell Resource Repositories - Fixes [Issue #393](https://github.com/dsccommunity/ComputerManagementDsc/issues/393) - Computer diff --git a/README.md b/README.md index f3b14933..c6a76868 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ The **ComputerManagementDsc** module contains the following resources: predictably handle the condition. - **PowerPlan**: This resource allows specifying a power plan to activate. - **PowerShellExecutionPolicy**: Specifies the desired PowerShell execution policy. +- **PSResource**: This resource manages PowerShell Resources, like modules. - **PSResourceRepository**: This resource manages PowerShellGet repositories. - **RemoteDesktopAdmin**: This resource will manage the remote desktop administration settings on a computer. @@ -86,3 +87,7 @@ Management Framework 5.0 or above. ### PSResourceRepository The resource `PSResourceRepository` requires that the PowerShell modules `PowerShellGet` and `PackageManagement` are already present on the target computer. + +### PSResource + +The resource `PSResource` requires that the PowerShell modules `PowerShellGet` and `PackageManagement` are already present on the target computer. diff --git a/source/Classes/020.PSResource.ps1 b/source/Classes/020.PSResource.ps1 new file mode 100644 index 00000000..1b07d9b3 --- /dev/null +++ b/source/Classes/020.PSResource.ps1 @@ -0,0 +1,885 @@ +<# + .SYNOPSIS + The `PSResource` Dsc resource is used to manage PowerShell resources on a server. + + .PARAMETER Ensure + If the resource should be present or absent on the server + being configured. Default values is 'Present'. + + .PARAMETER Name + Specifies the name of the resource to manage. + + .PARAMETER Repository + Specifies the name of the PSRepository where the resource can be found. + + .PARAMETER RequiredVersion + Specifies the version of the resource you want to install or uninstall + + .PARAMETER MaximumVersion + Specifies the maximum version of the resource you want to install or uninstall. + + .PARAMETER MinimumVersion + Specifies the minimum version of the resource you want to install or uninstall. + + .PARAMETER Latest + Specifies whether to use the latest available version of the resource. + + .PARAMETER Force + Forces the installation of resource. If a resource of the same name and version already exists on the computer, + this parameter overwrites the existing resource with one of the same name that was found by the command. + + .PARAMETER AllowClobber + Allows the installation of resource regardless of if other existing resource on the computer have cmdlets + of the same name. + + .PARAMETER SkipPublisherCheck + Allows the installation of resource that have not been catalog signed. + + .PARAMETER OnlySingleVersion + Specifies whether only a single version of the resource should installed be on the server. + + .PARAMETER AllowPrerelease + Specifies whether to allow pre-release versions of the resource. + + .PARAMETER Credential + Specifies credentials of an account that has rights to the repository. + + .PARAMETER Proxy + Specifies the URI of the proxy to connect to the repository. + + .PARAMETER ProxyCredential + Specifies the Credential to connect to the repository proxy. + + .PARAMETER RemoveNonCompliantVersions + Specifies whether to remove resources that do not meet criteria of MinimumVersion, MaximumVersion, or RequiredVersion + + .EXAMPLE + Invoke-DscResource -ModuleName ComputerManagementDsc -Name PSResource -Method Get -Property @{ + Name = 'PowerShellGet' + Repository = 'PSGallery' + RequiredVersion = '2.2.5' + Force = $true + SkipPublisherCheck = $false + OnlySingleVersion = $true + AllowPrerelease = $False + } + This example shows how to call the resource using Invoke-DscResource. +#> +[DscResource()] +class PSResource : ResourceBase +{ + [DscProperty()] + [Ensure] + $Ensure = [Ensure]::Present + + [DscProperty(Key)] + [System.String] + $Name + + [DscProperty()] + [System.String] + $Repository + + [DscProperty()] + [System.String] + $RequiredVersion + + [DscProperty()] + [System.String] + $MaximumVersion + + [DscProperty()] + [System.String] + $MinimumVersion + + [DscProperty()] + [Nullable[System.Boolean]] + $Latest + + [DscProperty()] + [Nullable[System.Boolean]] + $Force + + [DscProperty()] + [Nullable[System.Boolean]] + $AllowClobber + + [DscProperty()] + [Nullable[System.Boolean]] + $SkipPublisherCheck + + [DscProperty()] + [Nullable[System.Boolean]] + $OnlySingleVersion + + [DscProperty()] + [Nullable[System.Boolean]] + $AllowPrerelease + + [DscProperty()] + [PSCredential] + $Credential + + [DscProperty()] + [System.String] + $Proxy + + [DscProperty()] + [PSCredential] + $ProxyCredential + + [DscProperty()] + [Nullable[System.Boolean]] + $RemoveNonCompliantVersions + + <# + Property for holding the latest version of the resource available + #> + hidden [Version] + $LatestVersion + + <# + Property for holding the given version requirement (MinimumVersion, MaximumVersion, RequiredVersion or Latest) if passed + #> + hidden [System.String] + $VersionRequirement + + PSResource () : base () + { + # These properties will not be enforced. + $this.ExcludeDscProperties = @( + 'Name' + 'AllowPrerelease' + 'SkipPublisherCheck' + 'AllowClobber' + 'Force' + 'Repository' + 'Credential' + 'Proxy' + 'ProxyCredential' + ) + + $this.VersionRequirement = $null + $this.LatestVersion = $null + } + + [PSResource] Get() + { + return ([ResourceBase]$this).Get() + } + + [void] Set() + { + ([ResourceBase]$this).Set() + } + + [Boolean] Test() + { + return ([ResourceBase] $this).Test() + } + + <# + The parameter properties will contain the properties that should be enforced and that are not in desired + state. + #> + hidden [void] Modify([System.Collections.Hashtable] $properties) + { + $installedResource = $this.GetInstalledResource() + + if ($properties.ContainsKey('Ensure') -and $properties.Ensure -eq 'Absent' -and $this.Ensure -eq 'Absent') + { + if ($properties.ContainsKey('RequiredVersion') -and $this.RequiredVersion) + { + $resourceToUninstall = $installedResource | Where-Object {$_.Version -eq [Version]$this.RequiredVersion} + $this.UninstallModule($resourceToUninstall) + } + foreach ($resource in installedResource) + { + $this.UninstallResource($resource) + } + } + elseif ($properties.ContainsKey('Ensure') -and $properties.Ensure -eq 'Present' -and $this.Ensure -eq 'Present') + { + # Module does not exist at all + $this.InstallResource() + } + elseif ($properties.ContainsKey('OnlySingleVersion')) + { + $this.ResolveOnlySingleVersion($installedResource) + } + elseif ($properties.ContainsKey('RemoveNonCompliantVersions') -and $this.RemoveNonCompliantVersions) + { + $this.UninstallNonCompliantVersions($installedResource) + + if ($this.VersionRequirement -in $properties.Keys) + { + $this.InstallResource() + } + } + else + { + $this.InstallResource() + } + } + + <# + The parameter properties will contain the key properties. + #> + hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) + { + $currentState = $this | Get-DscProperty -ExcludeName $this.ExcludeDscProperties -Type @('Key', 'Mandatory', 'Optional') -HasValue + + $resources = $this.GetInstalledResource() + + $returnValue = @{ + Name = $this.Name + Ensure = [Ensure]::Absent + } + + if ($currentState.ContainsKey('OnlySingleVersion') -and $currentState.OnlySingleVersion -and $this.OnlySingleVersion) + { + $returnValue.OnlySingleVersion = $this.TestOnlySingleVersion($resources) + } + else + { + # OnlySingleVersion has been passed as $False. Whether it is the OnlySingleVersion or not doesn't matter + $returnValue.OnlySingleVersion = $currentState.OnlySingleVersion + } + + if ($currentState.ContainsKey('Latest') -and $this.Latest -eq $true) + { + $returnValue.Latest = $this.TestLatestVersion($resources) + } + + if ($null -eq $resources) + { + Write-Verbose -Message ($this.localizedData.ResourceNotInstalled -f $this.Name) + } + else + { + $returnValue.Ensure = [Ensure]::Present + + if ($this | Get-DscProperty -Name @('RequiredVersion', 'MaximumVersion', 'MinimumVersion') -HasValue) + { + $returnValue.$($this.VersionRequirement) = $this.GetRequiredVersionFromVersionRequirement($resources, $this.VersionRequirement) + } + } + + if (-not [System.String]::IsNullOrEmpty($this.VersionRequirement) -and $currentState.ContainsKey('RemoveNonCompliantVersions')) + { + $returnValue.RemoveNonCompliantVersions = $this.TestVersionRequirement($resources, $this.VersionRequirement) + } + + return $returnValue + } + + <# + The parameter properties will contain the properties that was + assigned a value. + #> + hidden [void] AssertProperties([System.Collections.Hashtable] $properties) + { + Assert-Module -ModuleName PowerShellGet + Assert-Module -ModuleName PackageManagement + + if ($properties.ContainsKey('AllowPrerelease')) + { + Try + { + Import-Module -Name PowerShelllGet -MinimumVersion '1.6.0' -ErrorAction Stop + } + Catch + { + New-InvalidArgumentException -ArgumentName 'AllowPrerelease' -Message $this.localizedData.PowerShellGetVersionTooLowForAllowPrerelease + } + } + + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'RemoveNonCompliantVersions' + ) + MutuallyExclusiveList2 = @( + 'OnlySingleVersion' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'Latest' + ) + MutuallyExclusiveList2 = @( + 'MinimumVersion' + 'RequiredVersion' + 'MaximumVersion' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'MinimumVersion' + ) + MutuallyExclusiveList2 = @( + 'RequiredVersion' + 'Latest' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'MaximumVersion' + ) + MutuallyExclusiveList2 = @( + 'RequiredVersion' + 'Latest' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + $assertBoundParameterParameters = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @( + 'RequiredVersion' + ) + MutuallyExclusiveList2 = @( + 'MaximumVersion' + 'MinimumVersion' + ) + } + + Assert-BoundParameter @assertBoundParameterParameters + + if ($this.Ensure -eq 'Absent' -and ( + $properties.ContainsKey('MinimumVersion') -or + $properties.ContainsKey('MaximumVersion') -or + $properties.ContainsKey('Latest') + ) + ) + { + $errorMessage = $this.localizedData.EnsureAbsentWithVersioning + + New-InvalidArgumentException -ArgumentName 'Ensure' -Message $errorMessage + } + + if ($properties.ContainsKey('ProxyCredental') -and (-not $properties.ContainsKey('Proxy'))) + { + $errorMessage = $this.localizedData.ProxyCredentialPassedWithoutProxyUri + + New-InvalidArgumentException -ArgumentName 'ProxyCredential' -Message $errorMessage + } + + if ($properties.ContainsKey('Proxy') -or $properties.ContainsKey('Credential') -and (-not $properties.ContainsKey('Repository'))) + { + $errorMessage = $this.localizedData.ProxyorCredentialWithoutRepository + + New-InvalidArgumentException -ArgumentName 'Repository' -message $errorMessage + } + + if ($properties.ContainsKey('RemoveNonCompliantVersions') -and + -not ( + $properties.ContainsKey('MinimumVersion') -or + $Properties.ContainsKey('MaximumVersion') -or + $Properties.ContainsKey('RequiredVersion') -or + $Properties.ContainsKey('Latest') + ) + ) + { + $errorMessage = $this.localizedData.RemoveNonCompliantVersionsWithoutVersioning + + New-InvalidArgumentException -ArgumentName 'RemoveNonCompliantVersions' -message $errorMessage + } + + <# + Is this the correct place to set hidden properties? I want to set them once rather than multiple times as required in the code + Assert() calls this before Get/Set/Test, so this ensures they're always set if necessary. + #> + if ($properties.ContainsKey('Latest')) + { + $this.LatestVersion = $this.GetLatestVersion() + } + + if ($properties.ContainsKey('MinimumVersion') -or + $Properties.ContainsKey('MaximumVersion') -or + $Properties.ContainsKey('RequiredVersion') -or + $Properties.ContainsKey('Latest') + ) + { + $this.VersionRequirement = $this.GetVersionRequirement() + } + } + + <# + Returns true if only the correct instance of the resource is installed on the system + #> + hidden [System.Boolean] TestOnlySingleVersion([System.Collections.Hashtable[]]$resources) + { + $return = $false #! Is this the correct default if somehow the if/else isn't triggered? + + if ($resources.Count -ne 1) + { + Write-Verbose -Message ($this.localizedData.ShouldBeOnlySingleVersion -f $this.Name, $resources.Count) + + $return = $false + } + else + { + # OnlySingleVersion should not rely on VersionRequirements to report correctly + $return = $true + } + + return $return + } + + <# + Get the latest version of the resource + #> + hidden [Version] GetLatestVersion() + { + Write-Verbose -Message ($this.LocalizedData.GetLatestVersion -f $this.Name) + $resource = $this.FindResource() + + Write-Verbose -Message ($this.LocalizedData.FoundLatestVersion -f $this.Name, $resource.Version) + + return $resource.Version + } + + <# + Get all instances of installed resource on the system + #> + hidden [System.Collections.Hashtable[]] GetInstalledResource() + { + $return = @() + $modules = Get-Module -Name $this.Name -ListAvailable + + foreach ($module in $modules) + { + $return += @{ + Name = $module.Name + Version = $module.Version + } + } + + return $return + } + + <# + Get full version as a string checking for prerelease version + #> + hidden [System.String] GetFullVersion([System.Collections.Hashtable] $resource) + { + $version = [System.String]$resource.Version + $prerelease = $resource.PrivateData.PSData.Prerelease + if (-not ([System.String]::IsNullOrEmpty($prerelease))) + { + $version = "$($version)-$($prerelease)" + } + return $version + } + + <# + Test whether a given resource is prerelease + #> + hidden [System.Boolean] TestPrerelease ([System.Collections.Hashtable] $resource) + { + $prerelease = $False + if (-not ([System.String]::IsNullOrEmpty($resource.PrivateData.PSData.Prerelease))) + { + $prerelease = $True + } + return $prerelease + } + + <# + tests whether the installed resources includes the latest version available + #> + hidden [System.Boolean] TestLatestVersion ([System.Collections.Hashtable[]] $resources) + { + $return = $false + + if ($this.LatestVersion -notin $resources.Version) + { + Write-Verbose -Message ($this.localizedData.ShouldBeLatest -f $this.Name, $this.LatestVersion) + } + else + { + Write-Verbose -Message ($this.localizedData.IsLatest -f $this.Name, $this.LatestVersion) + + $return = $true + } + + return $return + } + + <# + Tests whether the repository the resource will install from is trusted and if not if Force is set + #> + hidden [void] TestRepository () + { + if (-not $this.Force) + { + $resource = $this.FindResource() + + $resourceRepository = Get-PSRepository -Name $resource.Repository + + if ($resourceRepository.InstallationPolicy -eq 'Untrusted') + { + $errorMessage = $this.localizedData.UntrustedRepositoryWithoutForce + + New-InvalidArgumentException -Message ($errorMessage -f $resourceRepository.Name) -ArgumentName 'Force' + } + } + } + + <# + Find the latest resource on a PSRepository + #> + hidden [System.Collections.Hashtable] FindResource() + { + $params = $this | Get-DscProperty -ExcludeName @('Latest', 'OnlySingleVersion', 'Ensure', 'SkipPublisherCheck', 'Force', 'RemoveNonCompliantVersions') -Type Key,Optional -HasValue + $foundModule = Find-Module @params + + $return = @{ + Name = $foundModule.Name + Version = $foundModule.Version + Repository = $foundModule.Repository + } + + if ($this.AllowPrerelease -and $foundModule.AdditionalMetdata.IsPrerelease) + { + $return.Prerelease = $foundModule.Version.Split('-')[-1] + } + + return $return + } + + hidden [void] InstallResource() + { + $this.TestRepository() + + $params = $this | Get-DscProperty -ExcludeName @('Latest','OnlySingleVersion','Ensure','RemoveNonCompliantVersions') -Type Key,Optional -HasValue + Install-Module @params + } + + <# + Uninstall the given resource + #> + hidden [void] UninstallResource ([System.Collections.Hashtable]$resource) + { + Write-Verbose -Message ($this.localizedData.UninstallResource -f $resource.Name,$resource.Version) + Uninstall-Module -Name $resource.Name -RequiredVersion $resource.RequiredVersion -Force -AllowPreRelease + } + + <# + Checks whether all the installed resources meet the given versioning requirements of either MinimumVersion, MaximumVersion, or RequiredVersion + #> + hidden [System.Boolean] TestVersionRequirement ([System.Collections.Hashtable[]] $resources, [System.String] $requirement) + { + Write-Verbose -Message ($this.localizedData.TestVersionRequirement -f $requirement) + + $return = $true + + switch ($requirement) + { + 'MinimumVersion' { + foreach ($resource in $resources) + { + if ($resource.Version -lt [Version]$this.MinimumVersion) + { + Write-Verbose -Message ($this.localizedData.InstalledResourceDoesNotMeetMinimumVersion -f ($this.Name, $resource.Version, $this.MinimumVersion)) + + $return = $false + } + } + } + 'MaximumVersion' { + foreach ($resource in $resources) + { + if ($resource.Version -gt [Version]$this.MaximumVersion) + { + Write-Verbose -Message ($this.localizedData.InstalledResourceDoesNotMeetMaximumVersion -f ($this.Name, $resource.Version, $this.MaximumVersion)) + + $return = $false + } + } + } + 'RequiredVersion' { + foreach ($resource in $resources) + { + if ($resource.Version -ne [Version]$this.RequiredVersion) + { + Write-Verbose -Message ($this.localizedData.InstalledResourceDoesNotMeetRequiredVersion -f ($this.Name, $resource.Version, $this.RequiredVersion)) + + $return = $false + } + } + } + 'Latest' { + $nonCompliantVersions = ($resources | Where-Object {$_.Version -ne $this.LatestVersion}).Count + if ($nonCompliantVersions -gt 1) + { + Write-Verbose -Message ($this.localizedData.InstalledResourcesDoNotMeetLatestVersion -f ($nonCompliantVersions, $this.Name)) + + $return = $false + } + } + } + + return $return + } + + <# + Returns the minimum version of a resource installed on the system. + + If a resource matches the exact minimum version, that version is returned. + If no resources matches the exact minimum version, the eldest version is returned. + #> + hidden [System.String] GetMinimumInstalledVersion([System.Collections.Hashtable[]] $resources) + { + $return = $null + + foreach ($resource in $resources) + { + if ($resource.version -ge [version]$this.MinimumVersion) + { + $return = $this.MinimumVersion + + Write-Verbose -Message ($this.localizedData.MinimumVersionMet -f $this.Name, $return) + + break + } + + } + + if ([System.String]::IsNullOrEmpty($return)) + { + $return = $($resources | Sort-Object Version)[0].Version + + Write-Verbose -Message ($this.localizedData.MinimumVersionExceeded -f $this.Name, $this.MinimumVersion, $return) + } + + return $return + } + + <# + Returns the maximum version of a resource installed on the system. + + If a resource matches the exact maximum version, that version is returned. + If no resources matches the exact maximum version, the youngest version is returned. + #> + hidden [System.String] GetMaximumInstalledVersion([System.Collections.Hashtable[]] $resources) + { + $return = $null + + foreach ($resource in $resources) + { + if ($resource.version -le [version]$this.MaximumVersion) + { + $return = $this.MaximumVersion + + Write-Verbose -Message ($this.localizedData.MaximumVersionMet -f $this.Name, $return) + + break + } + } + + if ([System.String]::IsNullOrEmpty($return)) + { + $return = $($resources | Sort-Object Version -Descending)[0].Version + + Write-Verbose -Message ($this.localizedData.MaximumVersionExceeded -f $this.Name, $this.MaximumVersion, $return) + } + + return $return + } + + <# + Returns the required version of the resource if it is installed on the system. + #> + hidden [System.String] GetRequiredInstalledVersion([System.Collections.Hashtable[]] $resources) + { + $return = $null + + if ($this.RequiredVersion -in $resources.Version) + { + $return = $this.RequiredVersion + + Write-Verbose -Message ($this.localizedData.RequiredVersionMet -f $this.Name, $return) + } + + return $return + } + + hidden [System.String[]] GetMinimumAndMaximumInstalledVersion([System.Collections.Hashtable[]] $resources) + { + $return = $null + foreach ($resource in $resources) + { + if ($resource.Version -ge $this.MinimumVersion -and + $resource.Version -le $this.MaximumVersion + ) + { + Write-Verbose -Message ($this.LocalizedData.MinimumAndMaximumVersionMet -f $this.Name, $resource.Version) + $return += $resource.Version + } + } + + return $return + } + + <# + Return the resource's version requirement + #> + hidden [System.String[]] GetVersionRequirement() + { + $return = $null + + $versionProperties = @('Latest', 'MinimumVersion', 'MaximumVersion', 'RequiredVersion') + + foreach ($prop in $versionProperties) + { + if (-not [System.String]::IsNullOrEmpty($this.$prop)) + { + if ($prop -eq 'MinimumVersion' -and [System.String]::IsNullOrEmpty($this.MaximumVersion) -or + $prop -eq 'MaximumVersion' -and [System.String]::IsNullOrEmpty($this.MinimumVerison) + ) + { + $return = 'MinimumAndMaximumVersion' + } + else + { + $return = $prop + } + break + } + } + + Write-Verbose -Message ($this.localizedData.VersionRequirementFound -f $this.Name, $return) + + return $return + } + + <# + Returns the version that matches the given requirement from the installed resources. + #> + hidden [System.String] GetRequiredVersionFromVersionRequirement ([System.Collections.Hashtable[]] $resources, [System.String]$requirement) + { + $return = $null + + switch ($requirement) + { + 'MinimumVersion' + { + $return = $this.GetMinimumInstalledVersion($resources) + } + 'MaximumVersion' + { + $return = $this.GetMaximumInstalledVersion($resources) + } + 'RequiredVersion' + { + $return = $this.GetRequiredInstalledVersion($resources) + } + 'MinimumAndMaximum' + { + $return = $this.GetMinimumAndMaximumInstalledVersion($resources) + } + default + { + $errorMessage = ($this.localizedData.GetRequiredVersionFromVersionRequirementError -f $requirement) + New-InvalidArgumentException -Message $errorMessage -Argument 'versionRequirement' + } + } + + return $return + } + + <# + Get all resources that are not compliant based on VersionRequirement + #> + hidden [System.Collections.Hashtable[]] GetNonCompliantResources ([System.Collections.Hashtable[]] $resources) + { + $nonCompliantResources = $null + + switch ($this.VersionRequirement) + { + 'MinimumVersion' + { + $nonCompliantResources = $resources | Where-Object {$_.Version -lt [Version]$this.MinimumVersion} + } + 'MaximumVersion' + { + $nonCompliantResources = $resources | Where-Object {$_.Version -gt [Version]$this.MaximumVersion} + } + 'RequiredVersion' + { + $nonCompliantResources = $resources | Where-Object {$_.Version -ne $this.RequiredVersion} + } + 'Latest' + { + $nonCompliantResources = $resources | Where-Object {$_.Version -ne $this.LatestVersion} + } + 'MinimumAndMaximum' + { + $nonCompliantResources = $resources | Where-Object { + $_.Version -lt [Version]$this.MinimumVersion -or + $_Version -gt [Version]$this.MaximumVersion + } + } + } + + Write-Verbose -Message ($this.localizedData.NonCompliantVersionCount -f $nonCompliantResources.Count, $this.Name, $this.VersionRequirement) + + return $nonCompliantResources + } + + <# + Uninstall resources that do not match the given version requirement + #> + hidden [void] UninstallNonCompliantResources ([System.Collections.Hashtable[]] $resources) + { + $resourcesToUninstall = $this.GetNonCompliantResources($resources) + + foreach ($resource in $resourcesToUninstall) + { + Write-Verbose -Message ($this.localizedData.UninstallNonCompliantResource -f $resource.Name, $resource.Version, $this.VersionRequirement ) + $this.UninstallResource($resource) + } + } + + <# + Resolve single instance status. Find the required version, uninstall all others. Install required version is necessary. + #> + hidden [void] ResolveOnlySingleVersion ([System.Collections.Hashtable[]] $resources) + { + Write-Verbose -Message ($this.localizedData.ShouldBeOnlySingleVersion -f $this.Name, $resources.Count) + + # Too many versions + + $resourceToKeep = $this.FindResource() + + if ($resourceToKeep.Version -in $resources.Version) + { + $resourcesToUninstall = $resources | Where-Object {$_.Version -ne $resourceToKeep.Version} + } + else + { + $resourcesToUninstall = $resources + $this.InstallResource() + } + + foreach ($resource in $resourcesToUninstall) + { + $this.UninstallResource($resource) + } + } +} diff --git a/source/Examples/Resources/PSResource/2-Install_PackageManagement_RequiredVersion_Present.ps1 b/source/Examples/Resources/PSResource/2-Install_PackageManagement_RequiredVersion_Present.ps1 new file mode 100644 index 00000000..5ecf46bb --- /dev/null +++ b/source/Examples/Resources/PSResource/2-Install_PackageManagement_RequiredVersion_Present.ps1 @@ -0,0 +1,22 @@ +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + This configuration installs the resource PackageManagement with version 1.4.7 on a machine +#> + +configuration Install_PackageManagement_RequiredVersion_Present +{ + Import-DscResource -ModuleName 'ComputerManagementDsc' + + node localhost + { + PSResource 'Install_PackageManagement_RequiredVersion_Present' + { + Name = 'PackageManagement' + Ensure = 'Present' + Force = $true + RequiredVersion = '1.4.7' + } + } +} diff --git a/source/Examples/Resources/PSResource/3-Install_PackageManagement_Latest_Present.ps1 b/source/Examples/Resources/PSResource/3-Install_PackageManagement_Latest_Present.ps1 new file mode 100644 index 00000000..75b68f6e --- /dev/null +++ b/source/Examples/Resources/PSResource/3-Install_PackageManagement_Latest_Present.ps1 @@ -0,0 +1,22 @@ +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + This configuration installs the latest version of the resource PowerShellGet on a machine +#> + +configuration Install_PackageManagement_Latest_Present +{ + Import-DscResource -ModuleName 'ComputerManagementDsc' + + node localhost + { + PSResource 'Install_PackageManagement_Latest_Present' + { + Name = 'PowerShellGet' + Ensure = 'Present' + Force = $true + Latest = $true + } + } +} diff --git a/source/en-US/PSResource.strings.psd1 b/source/en-US/PSResource.strings.psd1 new file mode 100644 index 00000000..63f04aa3 --- /dev/null +++ b/source/en-US/PSResource.strings.psd1 @@ -0,0 +1,50 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + class PSResource. +#> + +ConvertFrom-StringData -StringData @' + PowerShellGetVersionTooLowForAllowPrerelease = The installed version of PowerShellGet does not support AllowPrerelease. Version 1.6.6 and higher is required. + GetLatestVersion = Getting latest version of resource '{0}'. + GetLatestVersionFromRepository = Getting latest version of resource '{0}' from repository '{1}'. + GetLatestVersionAllowPrerelease = Getting latest version of resource '{0}', including prerelease versions. + FoundLatestVersion = Latest version of resource '{0}' found is '{1}'. + ProxyCredentialPassedWithoutProxyUri = Proxy Credential passed without Proxy Uri. + UsingProxyToGetResource = Using proxy '{0}' to get resource '{1}'. + ProxyorCredentialWithoutRepository = Parameters Credential and Proxy may not be used without Repository. + ShouldBeSingleInstance = Resource '{0}' should be SingleInstance but is not. '{1}' instances of the resource are installed. + IsSingleInstance = Resource '{0}' is SingleInstance. + UntrustedRepositoryWithoutForce = Untrusted repository '{0}' requires the Force parameter to be true. + UninstallResource = Uninstalling resource '{0}' version '{1}'. + ShouldBeLatest = Resource '{0}' should have latest version '{1}' installed but doesn't. + IsLatest = Resource '{0}' has latest version '{1}' installed. + ResourceNotInstalled = Resource '{0}' is not present on system. + MinimumVersionMet = Resource '{0}' meets criteria of MinimumVersion '{1}'. + MinimumVersionExceeded = Resource '{0}' exceeds criteria of MinimumVersion '{1}', with version '{2}'. + MaximumVersionMet = Resource '{0}' meets criteria of MaximumVersion '{1}'. + MaximumVersionExceeded = Resource '{0}' exceeds criteria of MaximumVersion '{1}', with version '{2}'. + RequiredVersionMet = Resource '{0}' meets criteria of RequiredVersion '{1}'. + TestVersionRequirement = Testing if installed resources meets versioning requirement of '{0}'. + InstalledResourceDoesNotMeetMinimumVersion = Installed resource '{0}' with version '{1}' does not meet MinimumVersion requirement of '{2}'. + InstalledResourceDoesNotMeetMaximumVersion = Installed resource '{0}' with version '{1}' does not meet MaximumVersion requirement of '{2}'. + InstalledResourceDoesNotMeetRequiredVersion = Installed resource '{0}' with version '{1}' does not meet RequiredVersion requirement of '{2}'. + InstalledResourcesDoNotMeetLatestVersion = '{0}' installed resources of resource '{1}' do not meet Latest requirement. + EnsureAbsentWithVersioning = Parameters MinimumVersion, MaximumVersion, or Latest may not be used when Ensure is Absent. + TestRepositoryInstallationPolicy = Testing repository installation policy. + FindResource = Finding resource '{0}'. + InstallResource = Installing resource '{0}'. + GetInstalledResource = Getting all installed versions of resource '{0}'. + GetRequiredVersionFromVersionRequirementError = GetRequiredVersionFromVersionRequirement should only be used with 'MinimumVersion', 'MaximumVersion, and 'RequiredVersion', not '{0}'. + NonCompliantVersionCount = Found '{0}' instances of resource '{1}' that do not match version requirement of '{2}'. + RemoveNonCompliantVersionsWithoutVersioning = Argument 'RemoveNonCompliantVersions' requires one of parameters 'MinimumVersion', 'MaximumVersion', 'RequiredVersion' or 'Latest'. + VersionRequirementFound = Version requirement for resource '{0}' is '{1}'. + UninstallNonCompliantResource = Uninstalling version '{0}' of resource '{1}' because it does not match version requirement of '{2}'. + # GetMinimumAndMaximumInstalledVersion strings + MinimumAndMaximumVersionMet = Installed resource '{0}' version '{1}' meets MinimumVersion and MaximumVersion requirements. + # Modify() strings + ResourceShouldBeAbsentRequiredVersion = Resource '{0}' version '{1}' should be Absent but is Present. + ResourceShouldBeAbsent = Resource '{0}' should be Absent but is Present. + ResourceShouldBePresent = Resource '{0}' should be Present but is Absent. + +'@ diff --git a/tests/Unit/Classes/PSResource.Tests.ps1 b/tests/Unit/Classes/PSResource.Tests.ps1 new file mode 100644 index 00000000..a83d4a4f --- /dev/null +++ b/tests/Unit/Classes/PSResource.Tests.ps1 @@ -0,0 +1,2235 @@ +<# + .SYNOPSIS + Unit test for PSResource 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 'PSResource' { + Context 'When class is instantiated' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + { [PSResource]::new() } | Should -Not -Throw + } + } + + It 'Should have a default or empty constructor' { + InModuleScope -ScriptBlock { + $instance = [PSResource]::new() + $instance | Should -Not -BeNullOrEmpty + } + } + + It 'Should be the correct type' { + InModuleScope -ScriptBlock { + $instance = [PSResource]::new() + $instance.GetType().Name | Should -Be 'PSResource' + } + } + } + } + + Describe 'PSResource\Get()' -Tag 'Get' { + + Context 'When the system is in the desired state' { + } + + Context 'When the system is not in the desired state' { + } + } + + Describe 'PSResource\Set()' -Tag 'Set' { + } + + Describe 'PSResource\Test()' -Tag 'Test' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{ + Name = 'ComputerManagementDsc' + Repository = 'PSGallery' + } + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + + } + } + + It 'Should return $true' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Test() | Should -BeTrue + } + } + } + + Context 'When the system is not in the desired state' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + # Mock method Compare() which is called by the base method Test () + Add-Member -Force -MemberType 'ScriptMethod' -Name 'Compare' -Value { + return @{ + Property = 'Version' + ExpectedValue = '8.6.0' + ActualValue = '8.5.0' + } + } + } + } + + It 'Should return $false' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Test() | Should -BeFalse + } + } + } + } + + Describe 'PSResource\GetCurrentState()' -Tag 'GetCurrentState' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{ + Name = 'ComputerManagementDsc' + Repository = 'PSGallery' + } + } + } + Context 'When Ensure is present' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Ensure = [Ensure]::Present + } + } + + Context 'When OnlySingleVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.OnlySingleVersion = $true + } + } + + It 'Should return the correct current state when OnlySingleVersion is true' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @{ + Name = 'ComputerManagementDsc' + } + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'TestOnlySingleVersion' -Value { + return $true + } + + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.OnlySingleVersion | Should -BeTrue + } + } + } + + It 'Should return the correct current state when OnlySingleVersion is false because no resources are installed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'TestOnlySingleVersion' -Value { + return $false + } + + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.OnlySingleVersion | Should -BeFalse + } + } + } + + It 'Should return the correct current state when OnlySingleVersion is false because multiple resources are installed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '9.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'TestOnlySingleVersion' -Value { + return $false + } + + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.OnlySingleVersion | Should -BeFalse + } + } + } + } + + Context 'When VersionRequirement is set' { + + Context 'When Latest is true' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Latest = $true + } + } + + It 'Should return the correct state when Latest is true' { + + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'TestLatestVersion' -Value { + return $true + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.Latest | Should -BeTrue + } + } + } + + It 'Should return the correct state when Latest is false and multiple resources are installed' { + + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '9.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'TestLatestVersion' -Value { + return $false + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.Latest | Should -BeFalse + } + } + } + + It 'Should return the correct state when Latest is false and no resources are installed' { + + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'TestLatestVersion' -Value { + return $false + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.Latest | Should -BeFalse + } + } + } + } + + Context 'When MinimumVersion is true' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MinimumVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'MinimumVersion' + } + } + + It 'Should return the correct state when MinimumVersion requirement is met exactly' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '9.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MinimumVersion | Should -Be '9.0.0' + } + } + } + + It 'Should return the correct state when MinimumVersion requirement is met but not exactly' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '10.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MinimumVersion | Should -Be '9.0.0' + } + } + } + + It 'Should return the correct state when MinimumVersion requirement is not met and multiple resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '8.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '7.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MinimumVersion | Should -Be '7.0.0' + } + } + } + + It 'Should return the correct state when MinimumVersion requirement is not met and no resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return $null + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.MinimumVersion | Should -BeNullOrEmpty + } + } + } + } + + Context 'When MaximumVersion is true' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MaximumVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'MaximumVersion' + } + } + + It 'Should return the correct state when MaximumVersion requirement is met exactly' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '9.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MaximumVersion | Should -Be '9.0.0' + } + } + } + + It 'Should return the correct state when MaximumVersion requirement is met but not exactly' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '10.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MaximumVersion | Should -Be '9.0.0' + } + } + } + + It 'Should return the correct state when MaximumVersion requirement is not met and multiple resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '10.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '11.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '11.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MaximumVersion | Should -Be '11.0.0' + } + } + } + + It 'Should return the correct state when MaximumVersion requirement is not met and no resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return $null + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.MaximumVersion | Should -BeNullOrEmpty + } + } + } + } + + Context 'When RequiredVersion is true' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'RequiredVersion' + } + } + + It 'Should return the correct state when RequiredVersion requirement is met exactly' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '9.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '7.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.RequiredVersion | Should -Be '9.0.0' + } + } + } + + It 'Should return the correct state when RequiredVersion requirement is not met and multiple resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Name = 'ComputerManagementDsc' + Version = '10.0.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '11.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return $null + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.RequiredVersion | Should -BeNullOrEmpty + } + } + } + + It 'Should return the correct state when RequiredVersion requirement is not met and no resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return $null + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.RequiredVersion | Should -BeNullOrEmpty + } + } + } + } + + Context 'When RemoveNonCompliantVersions is true and MinimumVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MinimumVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'MinimumVersion' + $script:mockPSResourceInstance.RemoveNonCompliantVersions = $true + } + } + + It 'Should return the correct state when MinimumVersion and RemoveNonCompliantVersions is met' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @{ + Version = '9.0.0' + } + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'TestVersionRequirement' -Value { + return $true + } + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MinimumVersion | Should -Be '9.0.0' + $currentState.VersionRequirement | Should -Be 'MinimumVersion' + $currentState.RemoveNonCompliantVersions | Should -BeTrue + } + } + } + + It 'Should return the correct state when MinimumVersion is met and RemoveNonCompliantVersions is not' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Version = '9.0.0' + }, + @{ + Version = '10.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '9.0.0' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'TestVersionRequirement' -Value { + return $false + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MinimumVersion | Should -Be '9.0.0' + $currentState.VersionRequirement | Should -Be 'MinimumVersion' + $currentState.RemoveNonCompliantVersions | Should -BeFalse + } + } + } + + It 'Should return the correct state when MinimumVersion and RemoveNonCompliantVersions are not met and resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return @( + @{ + Version = '7.0.0' + }, + @{ + Version = '8.0.0' + } + ) + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return '7.0.0' + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'TestVersionRequirement' -Value { + return $false + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Present + $currentState.MinimumVersion | Should -Be '7.0.0' + $currentState.VersionRequirement | Should -Be 'MinimumVersion' + $currentState.RemoveNonCompliantVersions | Should -BeFalse + } + } + } + + It 'Should return the correct state when MinimumVersion and RemoveNonCompliantVersions are not met because no resources are installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'TestVersionRequirement' -Value { + return $false + } + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagementDsc' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.MinimumVersion | Should -BeNullOrEmpty + $currentState.VersionRequirement | Should -Be 'MinimumVersion' + $currentState.RemoveNonCompliantVersions | Should -BeFalse + } + } + } + } + + Context 'When RemoveNonCompliantVersions is true and MaximumVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MaximumVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'MaximumVersion' + $script:mockPSResourceInstance.RemoveNonCompliantVersions = $true + } + } + + It 'Should return the correct state when MaximumVersion and RemoveNonCompliantVersions is met' { + + } + + It 'Should return the correct state when MaximumVersion is met and RemoveNonCompliantVersions is not' { + + } + + It 'Should return the correct state when MaximumVersion and RemoveNonCompliantVersions are not met and resources are installed' { + + } + + It 'Should return the correct state when MaximumVersion and RemoveNonCompliantVersions are not met because no resources are installed' { + + } + } + + Context 'When RemoveNonCompliantVersions is true and RequiredVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'RequiredVersion' + $script:mockPSResourceInstance.RemoveNonCompliantVersions = $true + } + } + + It 'Should return the correct state when RequiredVersion and RemoveNonCompliantVersions is met' { + + } + + It 'Should return the correct state when RequiredVersion is met and RemoveNonCompliantVersions is not' { + + } + + It 'Should return the correct state when RequiredVersion and RemoveNonCompliantVersions are not met and resources are installed' { + + } + + It 'Should return the correct state when RequiredVersion and RemoveNonCompliantVersions are not met because no resources are installed' { + + } + } + + Context 'When RemoveNonCompliantVersions is true and Latest is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Latest = $true + $script:mockPSResourceInstance.VersionRequirement = 'Latest' + $script:mockPSResourceInstance.RemoveNonCompliantVersions = $true + } + } + + It 'Should return the correct state when Latest and RemoveNonCompliantVersions is met' { + + } + + It 'Should return the correct state when Latest is met and RemoveNonCompliantVersions is not' { + + } + + It 'Should return the correct state when Latest and RemoveNonCompliantVersions are not met and resources are installed' { + + } + + It 'Should return the correct state when Latest and RemoveNonCompliantVersions are not met because no resources are installed' { + + } + } + } + + Context 'When VersionRequirement is not set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + $script:mockPSResourceInstance.VersionRequirement = 'RequiredVersion' + $script:mockPSResourceInstance.RemoveNonCompliantVersions = $true + } + } + + It 'Should return the current state when RemoveNonCompliantVersions requirement is met' { + + } + + It 'Should return the current state when RemoveNonCompliantVersions requirement is not met and resource are installed' { + + } + + It 'Should return the current state when RemoveNonCompliantVersions requirement is not met and resources are not installed' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetInstalledResource' -Value { + return $null + } -PassThru | + Add-member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredVersionFromVersionRequirement' -Value { + return $null + } + + { + $currentState = $script:mockPSResourceInstance.GetCurrentState(@{Name = 'ComputerManagementDsc'}) + $currentState.Name | Should -Be 'ComputerManagement' + $currentState.Ensure | Should -Be [Ensure]::Absent + $currentState.RequiredVersion | Should -BeNullOrEmpty + $currentState.RemoveNonCompliantVersions | Should -BeFalse + } + } + } + } + + } + } + + Describe 'PSResource\Modify()' -Tag 'Modify' { + } + + Describe 'PSResource\AssertProperties()' -Tag 'AssertProperties' { + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{} + } + } + Context 'When PowerShellGet version is too low for AllowPrerelease' { + InModuleScope -ScriptBlock { + Mock -CommandName Get-Module -MockWith { + return @{ + Name = 'PowerShellGet' + Version = '1.5.0' + } + } + } + It 'Should throw the correct error' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AllowPrerelease = $true + $script:mockPSResourceInstance.AssertProperties( + @{AllowPrerelease = $true} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.PowerShellGetVersionTooLowForAllowPrerelease + } + } + } + } + + Context 'When passing dependant parameters' { + It 'Should throw when RemoveNonCompliantVersions and OnlySingleVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + RemoveNonCompliantVersions = $true + OnlySingleVersion = $true + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + + It 'Should throw when Latest and MinimumVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + Latest = $true + MinimumVersion = '1.0.0' + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + + It 'Should throw when Latest and RequiredVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + Latest = $true + RequiredVersion = '1.0.0' + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + + It 'Should throw when Latest and MaximumVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + Latest = $true + MaximumVersion = '1.0.0' + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + + It 'Should throw when MinimumVersion and MaximumVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + MinimumVersion = '1.0.0' + MaximumVersion = '1.0.0' + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + + It 'Should throw when MinimumVersion and RequiredVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + MinimumVersion = '1.0.0' + RequiredVersion = '1.0.0' + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + + It 'Should throw when RequiredVersion and MaximumVersion are passed together' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{ + MaximumVersion = '1.0.0' + RequiredVersion = '1.0.0' + } + ) | Should -Throw -ExpectedMessage 'DRC0010' + } + } + } + } + + Context 'When ensure is Absent' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Ensure = [Ensure]::Absent + } + } + + It 'Should throw when ensure is Absent and MinimumVersion is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{MinimumVersion = '1.0.0'} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.EnsureAbsentWithVersioning + } + } + } + + It 'Should throw when ensure is Absent and MaximumVersion is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{MaximumVersion = '1.0.0'} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.EnsureAbsentWithVersioning + } + } + } + + It 'Should throw when ensure is Absent and Latest is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{Latest = $true} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.EnsureAbsentWithVersioning + } + } + } + } + + Context 'When ProxyCredential is passed without Proxy' { + It 'Should throw when ProxyCredential is passed without Proxy' { + InModuleScope -ScriptBlock { + { + $securePassword = New-Object -Type SecureString + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'USER', $securePassword + + $script:mockPSResourceInstance.AssertProperties( + @{ProxyCredential = $credential} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.ProxyCredentialPassedWithoutProxyUri + } + } + } + } + + Context 'When Credential or Proxy are passed without Repository' { + It 'Should throw when Credential is passed without Repository' { + InModuleScope -ScriptBlock { + { + $securePassword = New-Object -Type SecureString + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList 'USER', $securePassword + + $script:mockPSResourceInstance.AssertProperties( + @{Credential = $credential} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.ProxyorCredentialWithoutRepository + } + } + } + + It 'Should throw when Proxy is passed without Repository' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{Proxy = 'http://psgallery.com/'} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.ProxyorCredentialWithoutRepository + } + } + } + } + + Context 'When RemoveNonCompliantVersions is passed without a versioning parameter' { + It 'Should throw when RemoveNonCompliantVersions is passed without a versioning parameter' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{RemoveNonCompliantVersions = $true} + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.RemoveNonCompliantVersionsWithoutVersioning + } + } + } + } + + Context 'When Latest is passed' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member 'ScriptMethod' -Name 'GetLatestVersion' -Value { + return '1.5.0' + } -Force + } + } + It 'Should correctly set read only LatestVersion property' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.AssertProperties( + @{Latest = $true} + ) + $script:mockPSResourceInstance.LatestVersion | Should -Be '1.5.0' + } + } + } + } + + Context 'When a versioning parameter is passed' { + It 'Should correctly set read only VersionRequirement property when MinimumVersion is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member 'ScriptMethod' -Name 'GetVersionRequirement' -Value { + return 'MinimumVersion' + } + + $script:mockPSResourceInstance.AssertProperties( + @{MinimumVersion = '1.1.0'} + ) + $script:mockPSResourceInstance.VersionRequirement | Should -Be 'MinimumVersion' + } + } + } + + It 'Should correctly set read only VersionRequirement property when MaximumVersion is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member 'ScriptMethod' -Name 'GetVersionRequirement' -Value { + return 'MaximumVersion' + } + + $script:mockPSResourceInstance.AssertProperties( + @{MaximumVersion = '1.1.0'} + ) + $script:mockPSResourceInstance.VersionRequirement | Should -Be 'MaximumVersion' + } + } + } + + It 'Should correctly set read only VersionRequirement property when RequiredVersion is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member 'ScriptMethod' -Name 'GetVersionRequirement' -Value { + return 'RequiredVersion' + } + + $script:mockPSResourceInstance.AssertProperties( + @{MaximumVersion = '1.1.0'} + ) + $script:mockPSResourceInstance.VersionRequirement | Should -Be 'RequiredVersion' + } + } + } + + It 'Should correctly set read only VersionRequirement property when Latest is passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance | + Add-Member 'ScriptMethod' -Name 'GetVersionRequirement' -Value { + return 'Latest' + } + + $script:mockPSResourceInstance.AssertProperties( + @{Latest = $true} + ) + $script:mockPSResourceInstance.VersionRequirement | Should -Be 'Latest' + } + } + } + } + } + + # Describe 'PSResource\TestOnlySingleVersion()' -Tag 'TestOnlySingleVersion' { + # InModuleScope -ScriptBlock { + # $script:mockPSResourceInstance = [PSResource] @{ + # Name = 'ComputerManagementDsc' + # Ensure = [Ensure]::Present + # OnlySingleVersion = $True + # } + # } + + # It 'Should not throw and return True when one resource is present' { + # InModuleScope -ScriptBlock { + # $script:mockPSResourceInstance.TestOnlySingleVersion( + # @( + # @{ + # Name = 'ComputerManagementDsc' + # Version = '8.6.0' + # } + # ) + # ) | Should -BeTrue + # } + # } + + # It 'Should not throw and return False when zero resources are present' { + # InModuleScope -ScriptBlock { + # $script:mockPSResourceInstance.TestOnlySingleVersion( + # @() + # ) | Should -BeFalse + # } + # } + + # It 'Should not throw and return False when more than one resource is present' { + # InModuleScope -ScriptBlock { + # $script:mockPSResourceInstance.TestOnlySingleVersion( + # @( + # @{ + # Name = 'ComputerManagementDsc' + # Version = '8.6.0' + # }, + # @{ + # Name = 'ComputerManagementDsc' + # Version = '8.5.0' + # } + # ) + # ) | Should -BeFalse + # } + # } + # } + + Describe 'PSResource\TestOnlySingleVersion()' -Tag 'TestOnlySingleVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{ + Name = 'ComputerManagementDsc' + Ensure = [Ensure]::Present + OnlySingleVersion = $True + } + } + } + + Context 'When there are zero resources installed' { + It 'Should Correctly return False when Zero Resources are Installed' { + + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.TestOnlySingleVersion($null) | Should -BeFalse + } + } + } + + Context 'When there is one resource installed' { + It 'Should Correctly return True when One Resource is Installed' { + InModuleScope -ScriptBlock { + $script:mockResources = @{Name = 'ComputerManagementDsc'} + $script:mockPSResourceInstance.TestOnlySingleVersion($script:mockResources) | Should -BeTrue + } + } + } + + Context 'When there are multiple resources installed' { + It 'Should Correctly return False' { + InModuleScope -ScriptBlock { + $script:mockResources = @{ + Name = 'ComputerManagementDsc' + Version = '8.5.0' + }, + @{ + Name = 'ComputerManagementDsc' + Version = '8.6.0' + } + $script:mockPSResourceInstance.TestOnlySingleVersion($script:mockResources) | Should -BeFalse + } + } + } + } + + Describe 'PSResource\GetLatestVersion()' -Tag 'GetLatestVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{ + Name = 'ComputerManagementDsc' + Ensure = [Ensure]::Present + } + } + } + + Context 'When there FindResource finds a resourse' { + # BeforeEach { + # Mock -CommandName Find-Module -MockWith { + # return $(New-MockObject -Type 'Version' | Add-Member -MemberType NoteProperty -Name 'Version' -Value '8.6.0') + # } + # } + + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'FindResource' -Value { + return [System.Collections.Hashtable] @{ + Version = '8.6.0' + } + } + + $script:mockPSResourceInstance.GetLatestVersion() | Should -Be '8.6.0' + } + } + } + + Context 'When there FindResource does not find a resourse' { + It 'Should return null or empty' { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'FindResource' -Value { + return $null + } + + $script:mockPSResourceInstance.GetLatestVersion() | Should -BeNullOrEmpty + } + } + } + } + + Describe 'PSResource\GetInstalledResource()' -Tag 'GetInstalledResource' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{ + Name = 'ComputerManagementDsc' + Ensure = [Ensure]::Present + } + } + } + + It 'Should return nothing' { + InModuleScope -ScriptBlock { + Mock -CommandName Get-Module + { $script:mockPSResourceInstance.GetInstalledResource() | Should -BeNullOrEmpty } + } + } + + It 'Should return one object' { + InModuleScope -ScriptBlock { + Mock -CommandName Get-Module -MockWith { + return @{ + Name = 'PowerShellGet' + Version = '3.0.17' + } + } + { + $resources = $script:mockPSResourceInstance.GetInstalledResource().Count + $resources.Count | Should -Be 1 + $resource.Name | Should -Be 'PowerShellGet' + $resource.Version | Should -Be '3.0.17' + } + } + } + + It 'Should return two objects' { + InModuleScope -ScriptBlock { + Mock -CommandName Get-Module -MockWith { + return @( + @{ + Name = 'PowerShellGet' + Version = '3.0.17' + }, + @{ + Name = 'PowerShellGet' + Version = '2.2.5' + } + ) + } + { + $resources = $script:mockPSResourceInstance.GetInstalledResource().Count + $resources.Count | Should -Be 2 + $resource[0].Name | Should -Be 'PowerShellGet' + $resource[0].Version | Should -Be '3.0.17' + $resource[1].Name | Should -Be 'PowerShellGet' + $resource[1].Version | Should -Be '2.2.5' + } + } + } + } + + Describe 'PSResource\GetFullVersion()' -Tag 'GetFullVersion' { + } + + Describe 'PSResource\TestPrerelease()' -Tag 'TestPrerelease' { + + } + + Describe 'PSResource\TestLatestVersion()' -Tag 'TestLatestVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource] @{ + Name = 'ComputerManagementDsc' + LatestVersion = '8.6.0' + Ensure = [Ensure]::Present + } + } + } + + It 'Should return true when only one resource is installed and it is the latest version' { + InModuleScope -ScriptBlock { + $script:mockInstalledResources = @{ + Name = 'PowerShellGet' + Version = '8.6.0' + } + $script:mockPSResourceInstance.TestLatestVersion($script:mockInstalledResources) | Should -BeTrue + } + } + + It 'Should return true when multiple resources are installed, including the latest version' { + InModuleScope -ScriptBlock { + + $script:mockInstalledResources = @( + @{ + Name = 'PowerShellGet' + Version = '8.1.0' + }, + @{ + Name = 'PowerShellGet' + Version = '8.6.0' + }, + @{ + Name = 'PowerShellGet' + Version = '8.7.0' + } + ) + + $script:mockPSResourceInstance.TestLatestVersion($script:mockInstalledResources) | Should -BeTrue + } + } + + It 'Should return false when only one resource is installed and it is not the latest version' { + InModuleScope -ScriptBlock { + $script:mockInstalledResources = @{ + Name = 'PowerShellGet' + Version = '8.5.0' + } + $script:mockPSResourceInstance.TestLatestVersion($script:mockInstalledResources) | Should -BeFalse + } + } + + It 'Should return false when multiple resources are installed, not including the latest version' { + InModuleScope -ScriptBlock { + $script:mockInstalledResources = @( + @{ + Name = 'PowerShellGet' + Version = '8.1.0' + }, + @{ + Name = 'PowerShellGet' + Version = '8.5.0' + }, + @{ + Name = 'PowerShellGet' + Version = '8.7.0' + } + ) + + $script:mockPSResourceInstance.TestLatestVersion($script:mockInstalledResources) | Should -BeFalse + } + } + } + + Describe 'PSResource\TestRepository()' -Tag 'TestRepository' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + } + } + + Context 'When the repository is untrusted' { + It 'Should throw when the repository is untrusted and force is not set' { + InModuleScope -ScriptBlock { + { + Mock -CommandName Get-PSRepository -MockWith { + return @{ + Name = 'PSGallery' + InstallationPolicy = 'Untrusted' + } + } + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'FindResource' -Value { + return @{ + Name = 'PowerShellGet' + Version = '1.5.0' + Repository = 'PSGallery' + } + } -Force + + $script:mockPSResourceInstance.TestRepository() | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.UntrustedRepositoryWithoutForce + } + } + } + + It 'Should not throw when the repository is untrusted and force is set' { + InModuleScope -ScriptBlock { + { + Mock -CommandName Get-PSRepository -MockWith { + return @{ + Name = 'PSGallery' + InstallationPolicy = 'Untrusted' + } + } + $script:mockPSResourceInstance.Force = $true + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'FindResource' -Value { + return @{ + Name = 'PowerShellGet' + Version = '1.5.0' + Repository = 'PSGallery' + } + } -Force + + $script:mockPSResourceInstance.TestRepository() | Should -Not -Throw + } + } + } + } + + Context 'When the repository is trusted' { + It 'Should not throw when the repository is trusted' { + InModuleScope -ScriptBlock { + { + Mock -CommandName Get-PSRepository -MockWith { + return @{ + Name = 'InternalRepo' + InstallationPolicy = 'Trusted' + } + } + $script:mockPSResourceInstance.Force = $true + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'FindResource' -Value { + return @{ + Name = 'PowerShellGet' + Version = '1.5.0' + Repository = 'InternalRepo' + } + } -Force + + $script:mockPSResourceInstance.TestRepository() | Should -Not -Throw + } + } + } + } + + } + + Describe 'PSResource\FindResource' -Tag 'FindResource' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + } + } + + Context 'When FindResource is called' { + It 'Should not throw and return properties correctly' { + InModuleScope -ScriptBlock { + Mock -CommandName Find-Module -MockWith { + return @{ + Name = 'ComputerManagementDsc' + Version = '9.0.0' + Repository = 'PSGallery' + } + } + + { + $findResourceResult = $script:mockPSResourceInstance.FindResource() + $findResourceResult.Name | Should -Be 'ComputerManagementDsc' + $findResourceResult.Version | Should -Be '9.0.0' + $findResourceResult.Repository | Should -Be 'PSGallery' + } + } + } + } + } + + Describe 'PSResource\InstallResource' -Tag 'InstallResource' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'TestRepository' -Value { + return $null #! Do I even need a -Value {} here? + } + + Mock -CommandName Install-Module + } + } + + Context 'When InstallResource is called' { + It 'Should not throw when InstallResource is called' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.InstallResource() | Should -Not -Throw + } + } + } + } + } + + Describe 'PSResource\UninstallResource' -Tag 'UninstallResource' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + + Mock -CommandName Uninstall-Module + } + } + + Context 'When UninstallResource is called' { + It 'Should not throw when UninstallResource is called' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.UninstallResource( + @{ + Name = 'ComputerManagementDsc' + Version = '1.6.0' + } + ) | Should -Not -Throw + } + } + } + } + } + + Describe 'PSResource\TestVersionRequirement()' -Tag 'TestVersionRequirement' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + + Mock -CommandName Uninstall-Module + } + } + + Context 'When versionrequirement is MinimumVersion' { + It 'Should return true when MinimumVersion requirement is met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.MinimumVersion = '8.6.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '8.6.0' + }, + 'MinimumVersion' + ) | Should -BeTrue + } + } + } + + It 'Should return false when MinimumVersion requirement is not met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.MinimumVersion = '8.7.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '8.6.0' + }, + 'MinimumVersion' + ) | Should -BeFalse + } + } + } + } + + Context 'When versionrequirement is MaximumVersion' { + It 'Should return true when MaximumVersion requirement is met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.MaximumVersion = '8.7.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '8.6.0' + }, + 'MaximumVersion' + ) | Should -BeTrue + } + } + } + + It 'Should return false when MaximumVersion requirement is not met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.MaximumVersion = '8.7.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '9.0.0' + }, + 'MaximumVersion' + ) | Should -BeFalse + } + } + } + } + + Context 'When versionrequirement is RequiredVersion' { + It 'Should return true when RequiredVersion requirement is met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '9.0.0' + }, + 'RequiredVersion' + ) | Should -BeTrue + } + } + } + + It 'Should return false when RequiredVersion requirement is not met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '8.0.0' + }, + 'RequiredVersion' + ) | Should -BeFalse + } + } + } + } + + Context 'When versionrequirement is Latest' { + It 'Should return true when Latest requirement is met' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.LatestVersion = '9.0.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '9.0.0' + }, + 'Latest' + ) | Should -BeTrue + } + } + } + + It 'Should return false when Latest requirement is not met with a single resource' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.Latest = '9.0.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @{ + Version = '8.0.0' + }, + 'Latest' + ) | Should -BeFalse + } + } + } + + It 'Should return false when Latest requirement is not met with a multiple resources, including latest' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.Latest = '9.0.0' + $script:mockPSResourceInstance.TestVersionRequirement( + @( + @{ + Version = '8.0.0' + }, + @{ + Version = '9.0.0' + } + ), + 'Latest' + ) | Should -BeFalse + } + } + } + } + } + + Describe 'PSResource\GetMinimumInstalledVersion()' -Tag 'GetMinimumInstalledVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{ + MinimumVersion = '7.0.0' + } + } + } + + Context 'When calling GetMinimumInstalledVersion()' { + It 'Should return the correct minimum version when an installed resource matches given MinimumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetMinimumInstalledVersion( + @( + @{ + Version = '6.0.0' + }, + @{ + Version = '7.0.0' + } + ) + ) | Should -Be '7.0.0' + } + } + } + + It 'Should return the correct minimum version when an installed resource does not match the given MinimumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetMinimumInstalledVersion( + @( + @{ + Version = '6.0.0' + }, + @{ + Version = '5.0.0' + }, + @{ + Version = '4.2.0' + } + ) + ) | Should -Be '4.2.0' + } + } + } + } + } + + Describe 'PSResource\GetMaximumInstalledVersion()' -Tag 'GetMaximumInstalledVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{ + MaximumVersion = '7.0.0' + } + } + } + + Context 'When calling GetMaximumInstalledVersion()' { + It 'Should return the correct maximum version when an installed resource matches given MaximumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetMaximumInstalledVersion( + @( + @{ + Version = '6.0.0' + }, + @{ + Version = '7.0.0' + } + ) + ) | Should -Be '7.0.0' + } + } + } + + It 'Should return the correct maximum version when an installed resource does not match the given MaximumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetMaximumInstalledVersion( + @( + @{ + Version = '6.0.0' + }, + @{ + Version = '7.0.0' + }, + @{ + Version = '4.2.0' + } + ) + ) | Should -Be '7.0.0' + } + } + } + } + } + + Describe 'PSResource\GetRequiredInstalledVersion()' -Tag 'GetRequiredInstalledVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{ + RequiredVersion = '7.0.0' + } + } + } + + Context 'When calling GetRequiredInstalledVersion()' { + It 'Should return the RequiredVersion when the required version is installed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetRequiredInstalledVersion( + @( + @{ + Version = '5.0.0' + }, + @{ + Version = '7.0.0' + }, + @{ + Version = '6.0.0' + } + ) + ) | Should -Be '7.0.0' + } + } + } + + It 'Should return null when the required version is not installed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetRequiredInstalledVersion( + @( + @{ + Version = '5.0.0' + }, + @{ + Version = '8.0.0' + }, + @{ + Version = '6.0.0' + } + ) + ) | Should -BeNullOrEmpty + } + } + } + } + } + + Describe 'PSResource\GetVersionRequirement()' -Tag 'GetVersionRequirement' { + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + } + } + + Context 'When Latest is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Latest = $true + } + } + It 'Should return Latest' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetVersionRequirement() | Should -Be 'Latest' + } + } + } + } + + Context 'When MinimumVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MinimumVersion = '9.0.0' + } + } + It 'Should return MinimumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetVersionRequirement() | Should -Be 'MinimumVersion' + } + } + } + } + + Context 'When MaximumVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MaximumVersion = '9.0.0' + } + } + It 'Should return MaximumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetVersionRequirement() | Should -Be 'MaximumVersion' + } + } + } + } + + Context 'When RequiredVersion is set' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + } + } + It 'Should return MaximumVersion' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetVersionRequirement() | Should -Be 'RequiredVersion' + } + } + } + } + + Context 'When no version requirement is set' { + It 'Should return null' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetVersionRequirement() | Should -BeNullOrEmpty + } + } + } + } + } + + Describe 'PSResource\GetRequiredVersionFromVersionRequirement' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + } + } + + Context 'When version requirement is MinimumVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetMinimumInstalledVersion' -Value { + return '9.0.0' + } + } + } + + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetRequiredVersionFromVersionRequirement( + @{Version = '9.0.0'}, + 'MinimumVersion' + ) | Should -Be '9.0.0' + } + } + } + } + + Context 'When version requirement is MaximumVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetMaximumInstalledVersion' -Value { + return '9.0.0' + } + } + } + + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetRequiredVersionFromVersionRequirement( + @{Version = '9.0.0'}, + 'MaximumVersion' + ) | Should -Be '9.0.0' + } + } + } + } + + Context 'When version requirement is RequiredVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetRequiredInstalledVersion' -Value { + return '9.0.0' + } + } + } + + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetRequiredVersionFromVersionRequirement( + @{Version = '9.0.0'}, + 'RequiredVersion' + ) | Should -Be '9.0.0' + } + } + } + } + + Context 'When version requirement is Latest' { + It 'Should return the throw correctly' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.GetRequiredVersionFromVersionRequirement( + @{Version = '9.0.0'}, + 'Latest' + ) | Should -Throw -ExpectedMessage $script:mockPSResourceInstance.localizedData.GetRequiredVersionFromVersionRequirementError + } + } + } + } + } + + Describe 'PSResource\GetNonCompliantResources()' -Tag 'GetNonCompliantVersions' { + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + + $script:resources = @( + @{Version = '9.0.0'}, + @{Version = '5.0.0'}, + @{Version = '3.0.0'}, + @{Version = '6.1.0'} + ) + } + } + + Context 'When version requirement is MinimumVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MinimumVersion = '9.0.0' + } + } + + It 'Should return the correct resources' { + InModuleScope -ScriptBlock { + { + $nonCompliantResources = $script:mockPSResourceInstance.GetNonCompliantResources($script:resources) + $nonCompliantResources.Count | Should -Be 3 + } + } + } + } + + Context 'When version requirement is MaximumVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.MaximumVersion = '9.0.0' + } + } + + It 'Should return the correct resources' { + InModuleScope -ScriptBlock { + { + $nonCompliantResources = $script:mockPSResourceInstance.GetNonCompliantResources($script:resources) + $nonCompliantResources.Count | Should -Be 0 + } + } + } + } + + Context 'When version requirement is RequiredVersion' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.RequiredVersion = '9.0.0' + } + } + + It 'Should return the correct resources' { + InModuleScope -ScriptBlock { + { + $nonCompliantResources = $script:mockPSResourceInstance.GetNonCompliantResources($script:resources) + $nonCompliantResources.Count | Should -Be 3 + } + } + } + } + + Context 'When version requirement is Latest' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance.Latest = $true + $script:mockPSResourceInstance.LatestVersion = '9.0.0' + } + } + + It 'Should return the correct resources' { + InModuleScope -ScriptBlock { + { + $nonCompliantResources = $script:mockPSResourceInstance.GetNonCompliantResources($script:resources) + $nonCompliantResources.Count | Should -Be 3 + } + } + } + } + } + + Describe 'PSResource\UninstallNonCompliantResources()' -Tag 'UninstallNonCompliantResources' { + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'UninstallResource' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetNonCompliantResources' -Value { + return $null + } + } + } + + Context 'When UninstallNonCompliantVerisons() is called' { + It 'Should not throw' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.UninstallNonCompliantResources(@{Version = '9.0.0'}) | Should -Not -Throw + } + } + } + } + } + + Describe 'PSResource\ResolveOnlySingleVersion' -Tag 'ResolveOnlySingleVersion' { + BeforeEach { + InModuleScope -ScriptBlock { + $script:mockPSResourceInstance = [PSResource]@{} + $script:mockPSResourceInstance | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'FindResource' -Value { + return @{ + Name = 'ComputerManagementDsc' + Verson = '9.0.0' + } + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'InstallResource' -Value { + return $null + } -PassThru | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'UninstallResource' -Value { + return $null + } + } + } + + Context 'When ResolveSingeInstance() is called' { + It 'Should not throw when no resources are passed' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.ResolveOnlySingleVersion(@()) | Should -Not -Throw + } + } + } + + It 'Should not throw when resources are passed and none match the resourceToKeep' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.ResolveOnlySingleVersion( + @( + @{Version = '8.0.0'}, + @{Version = '7.0.0'} + ) + ) | Should -Not -Throw + } + } + } + + It 'Should not throw when resources are passed and one matches the resourceToKeep' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.ResolveOnlySingleVersion( + @( + @{Version = '8.0.0'}, + @{Version = '9.0.0'} + ) + ) | Should -Not -Throw + } + } + } + + It 'Should not throw when a single resource is passed that matches the resourceToKeep' { + InModuleScope -ScriptBlock { + { + $script:mockPSResourceInstance.ResolveOnlySingleVersion( + @( + @{Version = '9.0.0'} + ) + ) | Should -Not -Throw + } + } + } + } + } +} +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 +}