diff --git a/CHANGELOG.md b/CHANGELOG.md index fba685844..f00151e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - SqlServerDsc - Updated pipeline files to support ModuleFast. + - `Get-SqlDscPreferredModule` + - Optionally specify what version of the the SQL preferred module to be imported using the SMODefaultModuleVersion environment variable ([issue #1965](https://github.com/dsccommunity/SqlServerDsc/issues/1965)). +- New private command: + - Get-SMOModuleCalculatedVersion - Returns the version of the SMO module as a string. SQLPS version 120 and 130 do not have the correct version set, so the file path is used to calculate the version. - SqlSetup - Added the parameter `SqlVersion` that can be used to set the SQL Server version to be installed instead of it looking for version in the setup @@ -41,6 +45,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated integration tests to use xPSDesiredStateConfiguration instead of PSDScResources. - SqlWindowsFirewall - Updated integration tests to use xPSDesiredStateConfiguration instead of PSDScResources. +- SqlServerDsc + - `Get-SqlDscPreferredModule` + - Now returns a PSModuleInfo object instead of just the module name. + - `Import-SqlDscPreferredModule` + - Handles PSModuleInfo objects from `Get-SqlDscPreferredModule` instead of strings. + - Sets -ErrorAction 'Stop' on Get-SqlDscPreferredModule to throw an error if no SQL module is found. The script-terminating error is caught and made into a statement-terminating error. + +### Remove + +- SqlServerDsc + - Removed PreferredModule_ModuleFound string in favor for more verbose PreferredModule_ModuleVersionFound. ## [16.4.0] - 2023-08-22 diff --git a/source/Private/Get-SMOModuleCalculatedVersion.ps1 b/source/Private/Get-SMOModuleCalculatedVersion.ps1 new file mode 100644 index 000000000..1af5156cb --- /dev/null +++ b/source/Private/Get-SMOModuleCalculatedVersion.ps1 @@ -0,0 +1,63 @@ +<# + .SYNOPSIS + Returns the calculated version of an SMO PowerShell module. + + .DESCRIPTION + Returns the calculated version of an SMO PowerShell module. + + For SQLServer, the version is calculated using the System.Version + field with '-preview' appended for pre-release versions . For + example: 21.1.1 or 22.0.49-preview + + For SQLPS, the version is calculated using the path of the module. For + example: + C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules + returns 130 + + .PARAMETER PSModuleInfo + Specifies the PSModuleInfo object for which to return the calculated version. + + .EXAMPLE + Get-SMOModuleCalculatedVersion -PSModuleInfo (Get-Module -Name 'sqlps') + + Returns the calculated version as a string. + + .OUTPUTS + [System.String] +#> +function Get-SMOModuleCalculatedVersion +{ + [OutputType([System.String])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Management.Automation.PSModuleInfo] + $PSModuleInfo + ) + + process + { + $version = $null + + if ($PSModuleInfo.Name -eq 'SQLPS') + { + <# + Parse the build version number '120', '130' from the Path. + Older version of SQLPS did not have correct versioning. + #> + $version = (Select-String -InputObject $PSModuleInfo.Path -Pattern '\\([0-9]{3})\\' -List).Matches.Groups[1].Value + } + else + { + $version = $PSModuleInfo.Version.ToString() + + if ($PSModuleInfo.PrivateData.PSData.Prerelease) + { + $version = '{0}-{1}' -f $PSModuleInfo.Version, $PSModuleInfo.PrivateData.PSData.Prerelease + } + } + + return $version + } +} diff --git a/source/Public/Get-SqlDscPreferredModule.ps1 b/source/Public/Get-SqlDscPreferredModule.ps1 index 446232927..08b1bb1ae 100644 --- a/source/Public/Get-SqlDscPreferredModule.ps1 +++ b/source/Public/Get-SqlDscPreferredModule.ps1 @@ -9,47 +9,45 @@ that name will be used as the preferred module name instead of the default module 'SqlServer'. + If the envrionment variable `SMODefaultModuleVersion` is set, then that + specific version of the preferred module will be searched for. + .PARAMETER Name Specifies the list of the (preferred) modules to search for, in order. Defaults to 'SqlServer' and then 'SQLPS'. .PARAMETER Refresh - Specifies if the session environment variable PSModulePath should be refresh + Specifies if the session environment variable PSModulePath should be refreshed with the paths from other environment variable targets (Machine and User). .EXAMPLE Get-SqlDscPreferredModule - Returns the module name SqlServer if it is installed, otherwise it will - return SQLPS if is is installed. If neither is installed `$null` is - returned. + Returns the SqlServer PSModuleInfo object if it is installed, otherwise it + will return SQLPS PSModuleInfo object if is is installed. If neither is + installed `$null` is returned. .EXAMPLE Get-SqlDscPreferredModule -Refresh - Updated the session environment variable PSModulePath and then returns the - module name SqlServer if it is installed, otherwise it will return SQLPS - if is is installed. If neither is installed `$null` is returned. + Updates the session environment variable PSModulePath and then returns the + SqlServer PSModuleInfo object if it is installed, otherwise it will return SQLPS + PSModuleInfo object if is is installed. If neither is installed `$null` is + returned. .EXAMPLE Get-SqlDscPreferredModule -Name @('MyModule', 'SQLPS') - Returns the module name MyModule if it is installed, otherwise it will - return SQLPS if is is installed. If neither is installed `$null` is - returned. + Returns the MyModule PSModuleInfo object if it is installed, otherwise it will + return SQLPS PSModuleInfo object if is is installed. If neither is installed + `$null` is returned. .NOTES - If the module SQLPS is specified (default value) the path is returned as - the module name. This is because importing 'SQLPS' using simply the name - could make the wrong version to be imported when several different version - of SQL Server is installed on the same node. To make sure the correct - (latest) version is imported the path to the latest version of SQLPS is - returned. The returned path can be passed directly to the parameter Name - of the command Import-Module. + #> function Get-SqlDscPreferredModule { - [OutputType([System.String])] + [OutputType([PSModuleInfo])] [CmdletBinding()] param ( @@ -92,79 +90,60 @@ function Get-SqlDscPreferredModule } } - $availableModuleName = $null + $availableModule = $null - $availableModule = Get-Module -FullyQualifiedName $Name -ListAvailable | - Select-Object -Property @( - 'Name', - 'Path', + $availableModules = Get-Module -Name $Name -ListAvailable | + ForEach-Object -Process { @{ - Name = 'Version' - Expression = { - if ($_.Name -eq 'SQLPS') - { - <# - Parse the build version number '120', '130' from the Path. - Older version of SQLPS did not have correct versioning. - #> - (Select-String -InputObject $_.Path -Pattern '\\([0-9]{3})\\' -List).Matches.Groups[1].Value - } - else - { - $versionToReturn = $_.Version - - if ($_.ContainsKey('PrivateData') -and $_.PrivateData.ContainsKey('PSData') -and $_.PrivateData.PSData.ContainsKey('Prerelease')) - { - if (-not [System.String]::IsNullOrEmpty($_.PrivateData.PSData.Prerelease)) - { - $versionToReturn = '{0}-{1}' -f $_.Version, $_.PrivateData.PSData.Prerelease - } - } - - $versionToReturn - } - } + PSModuleInfo = $_ + CalculatedVersion = $_ | Get-SMOModuleCalculatedVersion } - ) + } foreach ($preferredModuleName in $Name) { - $preferredModule = $availableModule | - Where-Object -Property 'Name' -EQ -Value $preferredModuleName + $preferredModules = $availableModules | + Where-Object -FilterScript { $_.PSModuleInfo.Name -eq $preferredModuleName } - if ($preferredModule) + if ($preferredModules) { - if ($preferredModule.Name -eq 'SQLPS') + if ($env:SMODefaultModuleVersion) { - # Get the latest version if available. - $preferredModule = $preferredModule | - Sort-Object -Property 'Version' -Descending | + # Get the version specified in $env:SMODefaultModuleVersion if available + $availableModule = $preferredModules | + Where-Object -FilterScript { $_.CalculatedVersion -eq $env:SMODefaultModuleVersion } | Select-Object -First 1 - - <# - For SQLPS the path to the module need to be returned as the - module name to be absolutely sure the latest version is used. - #> - $availableModuleName = Split-Path -Path $preferredModule.Path -Parent } else { - $availableModuleName = ($preferredModule | Select-Object -First 1).Name + # Get the latest version if available + $availableModule = $preferredModules | + Sort-Object -Property 'CalculatedVersion' -Descending | + Select-Object -First 1 } - Write-Verbose -Message ($script:localizedData.PreferredModule_ModuleFound -f $availableModuleName) + Write-Verbose -Message ($script:localizedData.PreferredModule_ModuleVersionFound -f $availableModule.PSModuleInfo.Name, $availableModule.CalculatedVersion) break } } - if (-not $availableModuleName) + if (-not $availableModule) { - $errorMessage = $script:localizedData.PreferredModule_ModuleNotFound + $errorMessage = $null + + if ($env:SMODefaultModuleVersion) + { + $errorMessage = $script:localizedData.PreferredModule_ModuleVersionNotFound -f $env:SMODefaultModuleVersion + } + else + { + $errorMessage = $script:localizedData.PreferredModule_ModuleNotFound + } # cSpell: disable-next Write-Error -Message $errorMessage -Category 'ObjectNotFound' -ErrorId 'GSDPM0001' -TargetObject ($Name -join ', ') } - return $availableModuleName + return $availableModule.PSModuleInfo } diff --git a/source/Public/Import-SqlDscPreferredModule.ps1 b/source/Public/Import-SqlDscPreferredModule.ps1 index 23fa1b69f..d2adff63d 100644 --- a/source/Public/Import-SqlDscPreferredModule.ps1 +++ b/source/Public/Import-SqlDscPreferredModule.ps1 @@ -70,7 +70,23 @@ function Import-SqlDscPreferredModule $getSqlDscPreferredModuleParameters.Refresh = $true } - $availableModuleName = Get-SqlDscPreferredModule @getSqlDscPreferredModuleParameters + $availableModule = $null + + try + { + $availableModule = Get-SqlDscPreferredModule @getSqlDscPreferredModuleParameters -ErrorAction 'Stop' + } + catch + { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + ($script:localizedData.PreferredModule_FailedFinding), + 'ISDPM0001', # cspell: disable-line + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + 'PreferredModule' + ) + ) + } if ($Force.IsPresent) { @@ -80,82 +96,65 @@ function Import-SqlDscPreferredModule if ($PSBoundParameters.ContainsKey('Name')) { - $removeModule += $Name + $removeModule += Get-Module -Name $Name } # Available module could be - if ($availableModuleName) + if ($availableModule) { - $removeModule += $availableModuleName + $removeModule += $availableModule } if ($removeModule -contains 'SQLPS') { - $removeModule += 'SQLASCmdlets' # cSpell: disable-line + $removeModule += Get-Module -Name 'SQLASCmdlets' # cSpell: disable-line } - Remove-Module -Name $removeModule -Force -ErrorAction 'SilentlyContinue' + Remove-Module -ModuleInfo $removeModule -Force -ErrorAction 'SilentlyContinue' } - - if ($availableModuleName) + else { - if (-not $Force.IsPresent) + <# + Check if the preferred module is already loaded into the session. + #> + $loadedModule = Get-Module -Name $availableModule.Name | Select-Object -First 1 + + if ($loadedModule) { - <# - Check if the preferred module is already loaded into the session. - If the module name is a path the leaf part must be used, which is - the module name. - #> - $loadedModuleName = (Get-Module -Name (Split-Path -Path $availableModuleName -Leaf) | Select-Object -First 1).Name - - if ($loadedModuleName) - { - Write-Verbose -Message ($script:localizedData.PreferredModule_AlreadyImported -f $loadedModuleName) - - return - } + Write-Verbose -Message ($script:localizedData.PreferredModule_AlreadyImported -f $loadedModule.Name) + + return } + } - try + try + { + Write-Debug -Message ($script:localizedData.PreferredModule_PushingLocation) + + Push-Location + + <# + SQLPS has unapproved verbs, disable checking to ignore Warnings. + Suppressing verbose so all cmdlet is not listed. + #> + $importedModule = Import-Module -ModuleInfo $availableModule -DisableNameChecking -Verbose:$false -Force:$Force -Global -PassThru -ErrorAction 'Stop' + + <# + SQLPS returns two entries, one with module type 'Script' and another with module type 'Manifest'. + Only return the object with module type 'Manifest'. + SqlServer only returns one object (of module type 'Script'), so no need to do anything for SqlServer module. + #> + if ($availableModule.Name -eq 'SQLPS') { - Write-Debug -Message ($script:localizedData.PreferredModule_PushingLocation) - - Push-Location - - <# - SQLPS has unapproved verbs, disable checking to ignore Warnings. - Suppressing verbose so all cmdlet is not listed. - #> - $importedModule = Import-Module -Name $availableModuleName -DisableNameChecking -Verbose:$false -Force:$Force -Global -PassThru -ErrorAction 'Stop' - - <# - SQLPS returns two entries, one with module type 'Script' and another with module type 'Manifest'. - Only return the object with module type 'Manifest'. - SqlServer only returns one object (of module type 'Script'), so no need to do anything for SqlServer module. - #> - if ($availableModuleName -eq 'SQLPS') - { - $importedModule = $importedModule | Where-Object -Property 'ModuleType' -EQ -Value 'Manifest' - } - - Write-Verbose -Message ($script:localizedData.PreferredModule_ImportedModule -f $importedModule.Name, $importedModule.Version, $importedModule.Path) + $importedModule = $importedModule | Where-Object -Property 'ModuleType' -EQ -Value 'Manifest' } - finally - { - Write-Debug -Message ($script:localizedData.PreferredModule_PoppingLocation) - Pop-Location - } + Write-Verbose -Message ($script:localizedData.PreferredModule_ImportedModule -f $importedModule.Name, $importedModule.Version, $importedModule.Path) } - else + finally { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.PreferredModule_FailedFinding), - 'ISDPM0001', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - 'PreferredModule' - ) - ) + Write-Debug -Message ($script:localizedData.PreferredModule_PoppingLocation) + + Pop-Location } } diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 3f7d23805..07e5bf4c5 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -148,8 +148,9 @@ ConvertFrom-StringData @' TraceFlag_Remove_NoCurrentTraceFlags = There are no current trace flags on instance. Nothing to remove. ## Get-SqlDscPreferredModule - PreferredModule_ModuleFound = Preferred module {0} found. + PreferredModule_ModuleVersionFound = Preferred module '{0}' with version '{1}' found. PreferredModule_ModuleNotFound = No preferred PowerShell module was found. +PreferredModule_ModuleVersionNotFound = No preferred Powershell module with version '{0}' was found. ## Import-SqlDscPreferredModule PreferredModule_ImportedModule = Imported PowerShell module '{0}' with version '{1}' from path '{2}'. diff --git a/tests/Unit/Private/Get-SMOModuleCalculatedVersion.Tests.ps1 b/tests/Unit/Private/Get-SMOModuleCalculatedVersion.Tests.ps1 new file mode 100644 index 000000000..a53b96b8f --- /dev/null +++ b/tests/Unit/Private/Get-SMOModuleCalculatedVersion.Tests.ps1 @@ -0,0 +1,137 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + 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.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:dscModuleName + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $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 + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Get-SMOModuleCalculatedVersion' -Tag 'Private' { + Context 'When passing in SQLServer module' { + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + + $sqlServerModule | Get-SMOModuleCalculatedVersion | Should -Be '21.1.18068' + } + } + + Context 'When module is in pre-release' { + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(22, 0, 49) + PrivateData = @{ + PSData = @{ + PreRelease = 'preview1' + } + } + } + + $sqlServerModule | Get-SMOModuleCalculatedVersion | Should -Be '22.0.49-preview1' + } + } + } + } + + Context 'When passing in SQLPS module' { + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + + $sqlServerModule | Get-SMOModuleCalculatedVersion | Should -Be '130' + } + } + } + + Context 'When passing in any other module' { + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'OtherModule' + Version = [Version]::new(1, 0, 0) + } + + $sqlServerModule | Get-SMOModuleCalculatedVersion | Should -Be '1.0.0' + } + } + + Context 'When module is in pre-release' { + It 'Should return the correct version' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'OtherModule' + Version = [Version]::new(1, 0, 0) + PrivateData = @{ + PSData = @{ + PreRelease = 'preview1' + } + } + } + + $sqlServerModule | Get-SMOModuleCalculatedVersion | Should -Be '1.0.0-preview1' + } + } + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 b/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 index a8923aa1a..6fb2f0ec1 100644 --- a/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscPreferredModule.Tests.ps1 @@ -117,121 +117,141 @@ Describe 'Get-SqlDscPreferredModule' -Tag 'Public' { Context 'When only first default preferred module is installed' { BeforeAll { + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SqlServer' - } + return $sqlServerModule } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be 'SqlServer' + It 'Should return the correct module' { + Get-SqlDscPreferredModule | Should -Be $sqlServerModule } } Context 'When only second default preferred module is installed' { BeforeAll { + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + return $sqlpsModule } } It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be ('C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS' -replace '\\', [IO.Path]::DirectorySeparatorChar) + Get-SqlDscPreferredModule | Should -Be $sqlpsModule } } Context 'When both default preferred modules are installed' { BeforeAll { + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SqlServer' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + $sqlServerModule, + $sqlpsModule ) } } It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be 'SqlServer' + Get-SqlDscPreferredModule | Should -Be $sqlServerModule } } Context 'When there are several installed versions of all default preferred modules' { BeforeAll { + $sqlServerModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + $sqlServerModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(22, 1, 1) + } + $sqlpsModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + $sqlpsModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SqlServer' - Version = '1.0.0' - } - @{ - Name = 'SqlServer' - Version = '2.0.0' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + $sqlServerModule1, + $sqlServerModule2, + $sqlpsModule1, + $sqlpsModule2 ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be 'SqlServer' + It 'Should return the latest version of the first default preferred module' { + Get-SqlDscPreferredModule | Should -Be $sqlServerModule2 } } Context 'When there are several installed versions of the first default preferred module' { BeforeAll { + $sqlServerModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + $sqlServerModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(22, 1, 1) + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SqlServer' - Version = '1.0.0' - } - @{ - Name = 'SqlServer' - Version = '2.0.0' - } + $sqlServerModule1, + $sqlServerModule2 ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be 'SqlServer' + It 'Should return the latest version of the first default preferred module' { + Get-SqlDscPreferredModule | Should -Be $sqlServerModule2 } } Context 'When there are several installed versions of the second default preferred module' { BeforeAll { + $sqlpsModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + $sqlpsModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + $sqlpsModule1, + $sqlpsModule2 ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be ('C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS' -replace '\\', [IO.Path]::DirectorySeparatorChar) + It 'Should return the latest version of the second default preferred module' { + Get-SqlDscPreferredModule | Should -Be $sqlpsModule2 } } } @@ -279,126 +299,246 @@ Describe 'Get-SqlDscPreferredModule' -Tag 'Public' { Context 'When only first preferred module is installed' { BeforeAll { + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SqlServer' - } + return $sqlServerModule } } It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Name 'SqlServer' | Should -Be 'SqlServer' + Get-SqlDscPreferredModule -Name 'SqlServer' | Should -Be $sqlServerModule } } Context 'When only second preferred module is installed' { BeforeAll { + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + return $sqlpsModule } } It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be ('C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS' -replace '\\', [IO.Path]::DirectorySeparatorChar) + Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be $sqlpsModule } } Context 'When both preferred modules are installed' { BeforeAll { + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SqlServer' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + $sqlServerModule, + $sqlpsModule ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be 'SqlServer' + It 'Should return the first preferred module' { + Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be $sqlServerModule } } Context 'When there are several installed versions of all preferred modules' { BeforeAll { + $sqlServerModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + $sqlServerModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(22, 1, 1) + PrivateData = @{ + PSData = @{ + PreRelease = 'preview1' + } + } + } + $sqlpsModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + $sqlpsModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SqlServer' - Version = '1.0.0' - } - @{ - Name = 'SqlServer' - Version = '2.0.0' - PrivateData = @{ - PSData = @{ - PreRelease = 'preview1' - } - } - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + $sqlServerModule1, + $sqlServerModule2, + $sqlpsModule1, + $sqlpsModule2 ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be 'SqlServer' + It 'Should return the latest first preferred module' { + Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be $sqlServerModule2 + } + + Context 'When the environment variable SMODefaultModuleVersion is assigned a module version' { + Context 'When the version of the module exists' { + BeforeAll { + $env:SMODefaultModuleVersion = '21.1.18068' + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleVersion' + } + + It 'Should return the specified module version' { + Get-SqlDscPreferredModule | Should -Be $sqlServerModule1 + } + } + + Context 'When the version of the module does not exist' { + BeforeAll { + $env:SMODefaultModuleVersion = '1.1.1' + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleVersion' + } + + It 'Should throw the correct error' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.PreferredModule_ModuleVersionNotFound + } + + { Get-SqlDscPreferredModule -ErrorAction 'Stop' } | Should -Throw -ExpectedMessage ($errorMessage -f $env:SMODefaultModuleVersion) + } + } } } Context 'When there are several installed versions of the first preferred module' { BeforeAll { + $sqlServerModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + $sqlServerModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(22, 1, 1) + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SqlServer' - Version = '1.0.0' - } - @{ - Name = 'SqlServer' - Version = '2.0.0' - } + $sqlServerModule1, + $sqlServerModule2 ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be 'SqlServer' + It 'Should return the latest version of the first preferred module' { + Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be $sqlServerModule2 + } + + Context 'When the environment variable SMODefaultModuleVersion is assigned a module version' { + Context 'When the version of the module exists' { + BeforeAll { + $env:SMODefaultModuleVersion = '21.1.18068' + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleVersion' + } + + It 'Should return the specified module version' { + Get-SqlDscPreferredModule | Should -Be $sqlServerModule1 + } + } + + Context 'When the version of the module does not exist' { + BeforeAll { + $env:SMODefaultModuleVersion = '1.1.1' + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleVersion' + } + + It 'Should throw the correct error' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.PreferredModule_ModuleVersionNotFound + } + + { Get-SqlDscPreferredModule -ErrorAction 'Stop' } | Should -Throw -ExpectedMessage ($errorMessage -f $env:SMODefaultModuleVersion) + } + } } } Context 'When there are several installed versions of the second preferred module' { BeforeAll { + $sqlpsModule1 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + $sqlpsModule2 = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-Module -MockWith { return @( - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } - @{ - Name = 'SQLPS' - Path = 'C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - } + $sqlpsModule1, + $sqlpsModule2 ) } } - It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be ('C:\Program Files (x86)\Microsoft SQL Server\160\Tools\PowerShell\Modules\SQLPS' -replace '\\', [IO.Path]::DirectorySeparatorChar) + It 'Should return the latest version of the second preferred module' { + Get-SqlDscPreferredModule -Name @('SqlServer', 'SQLPS') | Should -Be $sqlpsModule2 + } + + Context 'When the environment variable SMODefaultModuleVersion is assigned a module version' { + Context 'When the version of the module exists' { + BeforeAll { + $env:SMODefaultModuleVersion = '130' + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleVersion' + } + + It 'Should return the specified module version' { + Get-SqlDscPreferredModule | Should -Be $sqlpsModule1 + } + } + + Context 'When the version of the module does not exist' { + BeforeAll { + $env:SMODefaultModuleVersion = '999' + } + + AfterAll { + Remove-Item -Path 'env:SMODefaultModuleVersion' + } + + It 'Should throw the correct error' { + $errorMessage = InModuleScope -ScriptBlock { + $script:localizedData.PreferredModule_ModuleVersionNotFound + } + + { Get-SqlDscPreferredModule -ErrorAction 'Stop' } | Should -Throw -ExpectedMessage ($errorMessage -f $env:SMODefaultModuleVersion) + } + } } } } @@ -415,15 +555,18 @@ Describe 'Get-SqlDscPreferredModule' -Tag 'Public' { return 'MockPath' } + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SqlServer' - } + return $sqlServerModule } } It 'Should return the correct module name' { - Get-SqlDscPreferredModule -Refresh | Should -Be 'SqlServer' + Get-SqlDscPreferredModule -Refresh | Should -Be $sqlServerModule Should -Invoke -CommandName Set-PSModulePath -Exactly -Times 1 -Scope It } @@ -433,10 +576,13 @@ Describe 'Get-SqlDscPreferredModule' -Tag 'Public' { BeforeAll { $env:SMODefaultModuleName = 'OtherModule' + $otherModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = $env:SMODefaultModuleName + Version = [Version]::new(1, 1, 1) + } + Mock -CommandName Get-Module -MockWith { - return @{ - Name = $env:SMODefaultModuleName - } + return $otherModule } } @@ -445,7 +591,7 @@ Describe 'Get-SqlDscPreferredModule' -Tag 'Public' { } It 'Should return the correct module name' { - Get-SqlDscPreferredModule | Should -Be $env:SMODefaultModuleName + Get-SqlDscPreferredModule | Should -Be $otherModule } } } diff --git a/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 b/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 index bd2b48e2e..58face496 100644 --- a/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 +++ b/tests/Unit/Public/Import-SqlDscPreferredModule.Tests.ps1 @@ -89,8 +89,7 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { $mockImportModule = { - # Convert the single value array [String[]] to the expected string value [String]. - $moduleNameToImport = $Name[0] + $moduleNameToImport = $ModuleInfo.Name if ($moduleNameToImport -ne $mockExpectedModuleNameToImport) { @@ -141,18 +140,22 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { Context 'When module SqlServer is already loaded into the session' { BeforeAll { Mock -CommandName Import-Module - Mock -CommandName Get-SqlDscPreferredModule -MockWith { - return 'SqlServer' + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) } - Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SqlServer' - } + Mock -CommandName Get-SqlDscPreferredModule -MockWith { + return $sqlServerModule } } It 'Should use the already loaded module and not call Import-Module' { + Mock -CommandName Get-Module -MockWith { + return $sqlServerModule + } + { Import-SqlDscPreferredModule } | Should -Not -Throw Should -Invoke -CommandName Import-Module -Exactly -Times 0 -Scope It @@ -162,14 +165,18 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { Context 'When module SQLPS is already loaded into the session' { BeforeAll { Mock -CommandName Import-Module + + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + } + Mock -CommandName Get-SqlDscPreferredModule -MockWith { - return 'SQLPS' + return $sqlpsModule } Mock -CommandName Get-Module -MockWith { - return @{ - Name = 'SQLPS' - } + return $sqlpsModule } } @@ -184,8 +191,14 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { BeforeAll { Mock -CommandName Import-Module -MockWith $mockImportModule Mock -CommandName Get-Module + + $sqlServerModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SqlServer' + Version = [Version]::new(21, 1, 18068) + } + Mock -CommandName Get-SqlDscPreferredModule -MockWith { - return 'SqlServer' + return $sqlServerModule } $mockExpectedModuleNameToImport = 'SqlServer' @@ -205,8 +218,13 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { BeforeAll { Mock -CommandName Import-Module -MockWith $mockImportModule Mock -CommandName Get-Module + + $otherModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'OtherModule' + } + Mock -CommandName Get-SqlDscPreferredModule -MockWith { - return 'OtherModule' + return $otherModule } $mockExpectedModuleNameToImport = 'OtherModule' @@ -226,14 +244,20 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { BeforeAll { Mock -CommandName Import-Module -MockWith $mockImportModule Mock -CommandName Remove-Module + + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + Path = $sqlPsExpectedModulePath + } + Mock -CommandName Get-SqlDscPreferredModule -MockWith { - return $sqlPsExpectedModulePath + return $sqlpsModule } - $mockExpectedModuleNameToImport = $sqlPsExpectedModulePath + $mockExpectedModuleNameToImport = 'SQLPS' } - It 'Should import the SqlServer module without throwing' { + It 'Should import the SQLPS module without throwing' { { Import-SqlDscPreferredModule -Force } | Should -Not -Throw Should -Invoke -CommandName Get-SqlDscPreferredModule -ParameterFilter { @@ -251,7 +275,7 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { BeforeAll { Mock -CommandName Import-Module Mock -CommandName Get-Module - Mock -CommandName Get-SqlDscPreferredModule + Mock -CommandName Get-SqlDscPreferredModule { throw "Could not find the module" } } It 'Should throw the correct error message' { @@ -274,18 +298,23 @@ Describe 'Import-SqlDscPreferredModule' -Tag 'Public' { Mock -CommandName Import-Module -MockWith $mockImportModule Mock -CommandName Get-Module Mock -CommandName Remove-Module + + $sqlpsModule = New-MockObject -Type 'PSModuleInfo' -Properties @{ + Name = 'SQLPS' + } + Mock -CommandName Get-SqlDscPreferredModule -MockWith { - return 'SQLPS' + return $sqlpsModule } $mockExpectedModuleNameToImport = 'SQLPS' } - It 'Should import the SqlServer module without throwing' { + It 'Should import the SQLPD module without throwing' { { Import-SqlDscPreferredModule -Name 'OtherModule' -Force } | Should -Not -Throw Should -Invoke -CommandName Get-SqlDscPreferredModule -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Get-Module -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Get-Module -Exactly -Times 1 -Scope It Should -Invoke -CommandName Remove-Module -Exactly -Times 1 -Scope It Should -Invoke -CommandName Push-Location -Exactly -Times 1 -Scope It Should -Invoke -CommandName Pop-Location -Exactly -Times 1 -Scope It