diff --git a/CHANGELOG.md b/CHANGELOG.md index 079a7ecf1e..987072b04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ - Changes to xSQLServerHelper - The missing helper function ('Test-SPDSCObjectHasProperty'), that was referenced in the helper function Test-SQLDscParameterState, is now incorporated into Test-SQLDscParameterState (issue #589). - Changes to xSQLServer + - BREAKING CHANGE: xSQLServer does no longer try to support WMF 4.0 (PowerShell 4.0) (issue #574). Minimum supported version of WMF is now 5.0 (PowerShell 5.0). - BREAKING CHANGE: Removed deprecated resource xSQLAOGroupJoin (issue #457). - BREAKING CHANGE: Removed deprecated resource xSQLAOGroupEnsure (issue #456). - BREAKING CHANGE: Removed deprecated resource xSQLServerFailoverClusterSetup (issue #336). @@ -86,6 +87,11 @@ - Removed helper function New-ListenerADObject because the deprecated resource that was using it was removed. - Changes to xSQLServerSetup - BREAKING CHANGE: Replaced StartWin32Process helper function with the cmdlet Start-Process (issue #41, #93 and #126). + - BREAKING CHANGE: The parameter SetupCredential has been removed since it is no longer needed. This is because the resource now support the built-in PsDscRunAsCredential. + - BREAKING CHANGE: Now the resource supports using built-in PsDscRunAsCredential. If PsDscRunAsCredential is set, that username will be used as the first system administrator. + - BREAKING CHANGE: If the parameter PsDscRunAsCredential are not assigned any credentials then the resource will start the setup process as the SYSTEM account. When installing as the SYSTEM account, then parameter SQLSysAdminAccounts and ASSysAdminAccounts must be specified when installing feature Database Engine and Analysis Services respectively. + - When setup exits with exit code 3010 a warning message is written to console telling that setup finished successfully, but a reboot is required (partly fixes issue #565). + - Added a new parameter SetupProcessTimeout which defaults to 7200 seconds (2 hours). If the setup process has not finished before the timeout value in SetupProcessTimeout an error will be thrown (issue #566). ## 7.0.0.0 diff --git a/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.psm1 b/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.psm1 index 0bf6b59023..5d8e9b6de0 100644 --- a/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.psm1 +++ b/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.psm1 @@ -12,9 +12,6 @@ Import-Module -Name (Join-Path -Path (Split-Path -Path (Split-Path -Path $script .PARAMETER SourcePath The path to the root of the source files for installation. I.e and UNC path to a shared resource. Environment variables can be used in the path. - .PARAMETER SetupCredential - Credential to be used to perform the installation. - .PARAMETER SourceCredential Credentials used to access the path set in the parameter `SourcePath`. Using this parameter will trigger a copy of the installation media to a temp folder on the target node. Setup will then be started from the temp folder on the target node. @@ -45,10 +42,6 @@ function Get-TargetResource [System.String] $SourcePath, - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $SetupCredential, - [Parameter()] [System.Management.Automation.PSCredential] $SourceCredential, @@ -459,9 +452,6 @@ function Get-TargetResource .PARAMETER SourcePath The path to the root of the source files for installation. I.e and UNC path to a shared resource. Environment variables can be used in the path. - .PARAMETER SetupCredential - Credential to be used to perform the installation. - .PARAMETER SourceCredential Credentials used to access the path set in the parameter `SourcePath`. Using this parameter will trigger a copy of the installation media to a temp folder on the target node. Setup will then be started from the temp folder on the target node. @@ -590,6 +580,9 @@ function Get-TargetResource .PARAMETER FailoverClusterNetworkName Host name to be assigned to the clustered SQL Server instance + + .PARAMETER SetupProcessTimeout + The timeout, in seconds, to wait for the setup process to finish. Default value is 7200 seconds (2 hours). If the setup process does not finish before this time, and error will be thrown. #> function Set-TargetResource { @@ -607,10 +600,6 @@ function Set-TargetResource [System.String] $SourcePath, - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $SetupCredential, - [Parameter()] [System.Management.Automation.PSCredential] $SourceCredential, @@ -774,13 +763,16 @@ function Set-TargetResource [Parameter()] [System.String] - $FailoverClusterNetworkName + $FailoverClusterNetworkName, + + [Parameter()] + [System.UInt32] + $SetupProcessTimeout = 7200 ) $getTargetResourceParameters = @{ Action = $Action SourcePath = $SourcePath - SetupCredential = $SetupCredential SourceCredential = $SourceCredential InstanceName = $InstanceName FailoverClusterNetworkName = $FailoverClusterNetworkName @@ -864,8 +856,8 @@ function Set-TargetResource $sqlVersion = Get-SqlMajorVersion -Path $pathToSetupExecutable # Determine features to install - $featuresToInstall = "" - foreach ($feature in $Features.Split(",")) + $featuresToInstall = '' + foreach ($feature in $Features.Split(',')) { # Given that all the returned features are uppercase, make sure that the feature to search for is also uppercase $feature = $feature.ToUpper(); @@ -1174,7 +1166,16 @@ function Set-TargetResource # Should not be passed when PrepareFailoverCluster is specified if ($Action -in @('Install','InstallFailoverCluster','CompleteFailoverCluster')) { - $setupArguments += @{ SQLSysAdminAccounts = @($SetupCredential.UserName) } + if ($null -ne $PsDscContext.RunAsUser) + { + <# + Add the credentials from the parameter PsDscRunAsCredential, as the first + system administrator. The username is stored in $PsDscContext.RunAsUser. + #> + New-VerboseMessage -Message "Adding user '$($PsDscContext.RunAsUser)' from the parameter 'PsDscRunAsCredential' as the first system administrator account for SQL Server." + $setupArguments += @{ SQLSysAdminAccounts = @($PsDscContext.RunAsUser) } + } + if ($PSBoundParameters.ContainsKey('SQLSysAdminAccounts')) { $setupArguments['SQLSysAdminAccounts'] += $SQLSysAdminAccounts @@ -1232,7 +1233,15 @@ function Set-TargetResource if ($Action -in ('Install','InstallFailoverCluster','CompleteFailoverCluster')) { - $setupArguments += @{ ASSysAdminAccounts = @($SetupCredential.UserName) } + if ($null -ne $PsDscContext.RunAsUser) + { + <# + Add the credentials from the parameter PsDscRunAsCredential, as the first + system administrator. The username is stored in $PsDscContext.RunAsUser. + #> + New-VerboseMessage -Message "Adding user '$($PsDscContext.RunAsUser)' from the parameter 'PsDscRunAsCredential' as the first system administrator account for Analysis Services." + $setupArguments += @{ ASSysAdminAccounts = @($PsDscContext.RunAsUser) } + } if($PSBoundParameters.ContainsKey("ASSysAdminAccounts")) { @@ -1318,50 +1327,64 @@ function Set-TargetResource } } - New-VerboseMessage -Message "Starting setup using arguments: $log" - $arguments = $arguments.Trim() - $processArguments = @{ - FilePath = $pathToSetupExecutable - ArgumentList = $arguments - } - if ($Action -in @('InstallFailoverCluster','AddNode')) + try { - $processArguments.Add('Credential',$SetupCredential) - } + New-VerboseMessage -Message "Starting setup using arguments: $log" - $sqlSetupProcess = Start-Process @processArguments -PassThru -Wait -NoNewWindow - Wait-Process -InputObject $sqlSetupProcess -Timeout 120 + <# + This handles when PsDscRunAsCredential is set, or running as the SYSTEM account (when + PsDscRunAsCredential is set). + #> - $processExitCode = $sqlSetupProcess.ExitCode - $setupExitMessage = "Setup exited with code '$processExitCode'." + $startProcessParameters = @{ + FilePath = $pathToSetupExecutable + ArgumentList = $arguments + Timeout = $SetupProcessTimeout + } - if ($processExitCode -ne 0) { - $setupExitMessage += ' Please see the ''Summary.txt'' log file in the ''Setup Bootstrap\Log'' folder.' + $processExitCode = Start-SqlSetupProcess @startProcessParameters - throw $setupExitMessage - } - else - { - Write-Verbose $setupExitMessage - } + $setupExitMessage = "Setup exited with code '$processExitCode'." - if ($ForceReboot -or ($null -ne (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue))) - { - if (-not ($SuppressReboot)) + if ($processExitCode -eq 3010 -and -not $SuppressReboot) { - $global:DSCMachineStatus = 1 + $setupExitMessage = ('{0} {1}' -f $setupExitMessage, 'Setup was installed successfully, but a reboot is required.') + + Write-Warning -Message $setupExitMessage + } + elseif ($processExitCode -ne 0) + { + $setupExitMessage = ('{0} {1}' -f $setupExitMessage, 'Please see the ''Summary.txt'' log file in the ''Setup Bootstrap\Log'' folder.') + + throw $setupExitMessage } else { - New-VerboseMessage -Message 'Suppressing reboot' + New-VerboseMessage -Message $setupExitMessage } - } - if (-not (Test-TargetResource @PSBoundParameters)) + if ($ForceReboot -or ($null -ne (Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue))) + { + if (-not ($SuppressReboot)) + { + $global:DSCMachineStatus = 1 + } + else + { + New-VerboseMessage -Message 'Suppressing reboot' + } + } + + if (-not (Test-TargetResource @PSBoundParameters)) + { + throw New-TerminatingError -ErrorType TestFailedAfterSet -ErrorCategory InvalidResult + } + } + catch { - throw New-TerminatingError -ErrorType TestFailedAfterSet -ErrorCategory InvalidResult + throw $_ } } @@ -1376,9 +1399,6 @@ function Set-TargetResource .PARAMETER SourcePath The path to the root of the source files for installation. I.e and UNC path to a shared resource. Environment variables can be used in the path. - .PARAMETER SetupCredential - Credential to be used to perform the installation. - .PARAMETER SourceCredential Credentials used to access the path set in the parameter `SourcePath`. Using this parameter will trigger a copy of the installation media to a temp folder on the target node. Setup will then be started from the temp folder on the target node. @@ -1507,6 +1527,9 @@ function Set-TargetResource .PARAMETER FailoverClusterNetworkName Host name to be assigned to the clustered SQL Server instance + + .PARAMETER SetupProcessTimeout + The timeout, in seconds, to wait for the setup process to finish. Default value is 7200 seconds (2 hours). If the setup process does not finish before this time, and error will be thrown. #> function Test-TargetResource { @@ -1523,10 +1546,6 @@ function Test-TargetResource [System.String] $SourcePath, - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $SetupCredential, - [Parameter()] [System.Management.Automation.PSCredential] $SourceCredential, @@ -1690,13 +1709,16 @@ function Test-TargetResource [Parameter(ParameterSetName = 'ClusterInstall')] [System.String] - $FailoverClusterNetworkName + $FailoverClusterNetworkName, + + [Parameter()] + [System.UInt32] + $SetupProcessTimeout = 7200 ) $getTargetResourceParameters = @{ Action = $Action SourcePath = $SourcePath - SetupCredential = $SetupCredential SourceCredential = $SourceCredential InstanceName = $InstanceName FailoverClusterNetworkName = $FailoverClusterNetworkName @@ -1797,7 +1819,7 @@ function Get-FirstItemPropertyValue <# .SYNOPSIS - Copy folder structure using RoboCopy. Every file and folder, including empty ones are copied. + Copy folder structure using Robocopy. Every file and folder, including empty ones are copied. .PARAMETER Path Source path to be copied. @@ -1861,12 +1883,12 @@ function Copy-ItemWithRobocopy {$_ -gt 7 } { - throw "Robocopy reported that failures occured when copying files. Error code: $_." + throw "Robocopy reported that failures occurred when copying files. Error code: $_." } 1 { - Write-Verbose 'Robocopy copied files sucessfully' + Write-Verbose 'Robocopy copied files successfully' } 2 @@ -1876,7 +1898,7 @@ function Copy-ItemWithRobocopy 3 { - Write-Verbose 'Robocopy copied files to destination sucessfully. Robocopy also found files at the destination path that is not present at the source path, these extra files was remove at the destination path.' + Write-Verbose 'Robocopy copied files to destination successfully. Robocopy also found files at the destination path that is not present at the source path, these extra files was remove at the destination path.' } {$_ -eq 0 -or $null -eq $_ } @@ -2029,4 +2051,48 @@ function Get-ServiceAccountParameters return $parameters } +<# + .SYNOPSIS + Starts the SQL setup process- + + .PARAMETER FilePath + String containing the path to setup.exe. + + .PARAMETER ArgumentList + The arguments that should be passed to setup.exe. + + .PARAMETER Timeout + The timeout in seconds to wait for the process to finish. +#> +function Start-SqlSetupProcess +{ + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FilePath, + + [Parameter()] + [System.String] + $ArgumentList, + + [Parameter(Mandatory = $true)] + [System.UInt32] + $Timeout + ) + + $startProcessParameters = @{ + FilePath = $FilePath + ArgumentList = $ArgumentList + } + + $sqlSetupProcess = Start-Process @startProcessParameters -PassThru -NoNewWindow -ErrorAction Stop + + New-VerboseMessage -Message ('Started the process with id {0} using the path ''{1}'', and with a timeout value of {2} seconds.' -f $sqlSetupProcess.Id, $startProcessParameters.FilePath, $Timeout) + + Wait-Process -InputObject $sqlSetupProcess -Timeout $Timeout -ErrorAction Stop + + return $sqlSetupProcess.ExitCode +} + Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.schema.mof b/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.schema.mof index f30fd5590d..18ae419a0e 100644 --- a/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.schema.mof +++ b/DSCResources/MSFT_xSQLServerSetup/MSFT_xSQLServerSetup.schema.mof @@ -3,7 +3,6 @@ class MSFT_xSQLServerSetup : OMI_BaseResource { [Write, Description("The action to be performed. Default value is 'Install'."), ValueMap{"Install","InstallFailoverCluster","AddNode","PrepareFailoverCluster","CompleteFailoverCluster"}, Values{"Install","InstallFailoverCluster","AddNode","PrepareFailoverCluster","CompleteFailoverCluster"}] String Action; [Write, Description("The path to the root of the source files for installation. I.e and UNC path to a shared resource. Environment variables can be used in the path.")] String SourcePath; - [Required, EmbeddedInstance("MSFT_Credential"), Description("Credential to be used to perform the installation.")] String SetupCredential; [Write, EmbeddedInstance("MSFT_Credential"), Description("Credentials used to access the path set in the parameter 'SourcePath'.")] String SourceCredential; [Write, Description("Suppresses reboot.")] Boolean SuppressReboot; [Write, Description("Forces reboot.")] Boolean ForceReboot; @@ -50,5 +49,6 @@ class MSFT_xSQLServerSetup : OMI_BaseResource [Write, Description("Specifies the startup mode for SQL Server Browser service."), ValueMap{"Automatic", "Disabled", "Manual"}, Values{"Automatic", "Disabled", "Manual"}] String BrowserSvcStartupType; [Write, Description("The name of the resource group to create for the clustered SQL Server instance. Default is 'SQL Server (InstanceName)'.")] String FailoverClusterGroupName; [Write, Description("Array of IP Addresses to be assigned to the clustered SQL Server instance.")] String FailoverClusterIPAddress[]; - [Write, Description("Host name to be assigend to the clustered SQL Server instance.")] String FailoverClusterNetworkName; + [Write, Description("Host name to be assigned to the clustered SQL Server instance.")] String FailoverClusterNetworkName; + [Write, Description("The timeout, in seconds, to wait for the setup process to finish. Default value is 7200 seconds (2 hours). If the setup process does not finish before this time, and error will be thrown.")] Uint32 SetupProcessTimeout; }; diff --git a/README.md b/README.md index a8df20dcd1..115c707fb6 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,7 @@ Get-DscResource -Module xSQLServer ## Requirements -The minimum Windows Management Framework (PowerShell) version required is 4.0, which ships in Windows 8.1 or Windows Server 2012 R2 (or higher versions). But Windows Management Framework (PowerShell) 4.0 can also be installed on Windows Server 2008 R2. -The preferred Windows Management Framework (PowerShell) version is 5.0 or higher, which ships with Windows 10 or Windows Server 2016, but can also be installed on Windows 7 SP1, Windows 8.1, Windows Server 2008 R2 SP1, Windows Server 2012 and Windows Server 2012 R2. +The minimum Windows Management Framework (PowerShell) version required is 5.0 or higher, which ships with Windows 10 or Windows Server 2016, but can also be installed on Windows 7 SP1, Windows 8.1, Windows Server 2008 R2 SP1, Windows Server 2012 and Windows Server 2012 R2. ## Examples @@ -859,11 +858,20 @@ Installs SQL Server on the target node. > **Note:** It is not possible to add or remove features to a SQL Server failover cluster. This is a limitation of SQL Server. > See article [You cannot add or remove features to a SQL Server 2008, SQL Server 2008 R2, or SQL Server 2012 failover cluster](https://support.microsoft.com/en-us/help/2547273/you-cannot-add-or-remove-features-to-a-sql-server-2008,-sql-server-2008-r2,-or-sql-server-2012-failover-cluster). +#### Credentials + +##### PsDscRunAsCredential + +If PsDscRunAsCredential is set, the installation will be performed with those credentials, and the user name will be used as the first system administrator. + +##### SYSTEM + +If PsDscRunAsCredential is not assigned credentials then installation will be performed by the SYSTEM account. When installing as the SYSTEM account, then parameter SQLSysAdminAccounts and ASSysAdminAccounts must be specified when installing feature Database Engine and Analysis Services respectively. + #### Parameters * **[String] Action** _(Write)_: The action to be performed. Defaults to 'Install'. *Note: AddNode is not currently functional.* { _Install_ | InstallFailoverCluster | AddNode | PrepareFailoverCluster | CompleteFailoverCluster } * **[String] InstanceName** _(Key)_: SQL instance to be installed. -* **[PSCredential] SetupCredential** _(Required)_: Credential to be used to perform the installation. * **[String] SourcePath** _(Write)_: The path to the root of the source files for installation. I.e and UNC path to a shared resource. Environment variables can be used in the path. * **[PSCredential] SourceCredential** _(Write)_: Credentials used to access the path set in the parameter `SourcePath`. Using this parameter will trigger a copy of the installation media to a temp folder on the target node. Setup will then be started from the temp folder on the target node. For any subsequent calls to the resource, the parameter `SourceCredential` is used to evaluate what major version the file 'setup.exe' has in the path set, again, by the parameter `SourcePath`. To know how the temp folder is evaluated please read the online documentation for [System.IO.Path.GetTempPath()](https://msdn.microsoft.com/en-us/library/system.io.path.gettemppath(v=vs.110).aspx). If the path, that is assigned to parameter `SourcePath`, contains a leaf folder, for example '\\server\share\folder', then that leaf folder will be used as the name of the temporary folder. If the path, that is assigned to parameter `SourcePath`, does not have a leaf folder, for example '\\server\share', then a unique GUID will be used as the name of the temporary folder. * **[Boolean] SuppressReboot** _(Write)_: Suppresses reboot. @@ -905,6 +913,7 @@ Installs SQL Server on the target node. * **[String] FailoverClusterGroupName** _(Write)_: The name of the resource group to create for the clustered SQL Server instance. Default is 'SQL Server (_InstanceName_)'. * **[String[]]FailoverClusterIPAddress** _(Write)_: Array of IP Addresses to be assigned to the clustered SQL Server instance. IP addresses must be in [dotted-decimal notation](https://en.wikipedia.org/wiki/Dot-decimal_notation), for example ````10.0.0.100````. If no IP address is specified, uses 'DEFAULT' for this setup parameter. * **[String] FailoverClusterNetworkName** _(Write)_: Host name to be assigned to the clustered SQL Server instance. +* **[UInt32] SetupProcessTimeout** _(Write)_: The timeout, in seconds, to wait for the setup process to finish. Default value is 7200 seconds (2 hours). If the setup process does not finish before this time, and error will be thrown. #### Read-Only Properties from Get-TargetResource diff --git a/Tests/Unit/MSFT_xSQLServerSetup.Tests.ps1 b/Tests/Unit/MSFT_xSQLServerSetup.Tests.ps1 index bd36f789e0..497cf652eb 100644 --- a/Tests/Unit/MSFT_xSQLServerSetup.Tests.ps1 +++ b/Tests/Unit/MSFT_xSQLServerSetup.Tests.ps1 @@ -37,6 +37,72 @@ try Invoke-TestSetup InModuleScope $script:DSCResourceName { + <# + .SYNOPSIS + Used to test arguments passed to Start-SqlSetupProcess while inside and It-block. + + This function must be called inside a Mock, since it depends being run inside an It-block. + + .PARAMETER Argument + A string containing all the arguments separated with space and each argument should start with '/'. + Only the first string in the array is evaluated. + + .PARAMETER ExpectedArgument + A hash table containing all the expected arguments. + #> + function Test-SetupArgument + { + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Argument, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $ExpectedArgument + ) + + $argumentHashTable = @{} + + # Break the argument string into a hash table + ($Argument -split ' ?/') | ForEach-Object { + if ($_ -imatch '(\w+)="?([^/]+)"?') + { + $key = $Matches[1] + if ($key -in ('FailoverClusterDisks','FailoverClusterIPAddresses')) + { + $value = ($Matches[2] -replace '" "','; ') -replace '"','' + } + else + { + $value = ($Matches[2] -replace '" "',' ') -replace '"','' + } + + $null = $argumentHashTable.Add($key, $value) + } + } + + $actualValues = $argumentHashTable.Clone() + + # Start by checking whether we have the same number of parameters + Write-Verbose 'Verifying setup argument count (expected vs actual)' -Verbose + Write-Verbose -Message ('Expected: {0}' -f ($ExpectedArgument.Keys -join ',') ) -Verbose + Write-Verbose -Message ('Actual: {0}' -f ($actualValues.Keys -join ',')) -Verbose + + $actualValues.Count | Should Be $ExpectedArgument.Count + + Write-Verbose 'Verifying actual setup arguments against expected setup arguments' -Verbose + foreach ($argumentKey in $ExpectedArgument.Keys) + { + $argumentKeyName = $argumentHashTable.GetEnumerator() | Where-Object -FilterScript { $_.Name -eq $argumentKey } | Select-Object -ExpandProperty Name + $argumentKeyName | Should Be $argumentKey + + $argumentValue = $argumentHashTable.$argumentKey + $argumentValue | Should Be $ExpectedArgument.$argumentKey + } + } + # Testing each supported SQL Server version $testProductVersion = @( 14, # SQL Server "2017" @@ -108,8 +174,8 @@ try $mockAgentServicePassword = 'Ag3ntP@ssw0rd' $mockSQLAgentCredential = New-Object System.Management.Automation.PSCredential($mockAgentServiceAccount,($mockAgentServicePassword | ConvertTo-SecureString -AsPlainText -Force)) $mockAnalysisServiceAccount = 'COMPANY\AnalysisAccount' - $mockAnslysisServicePassword = 'Analysiss3v!c3P@ssw0rd' - $mockAnalysisServiceCredential = New-Object System.Management.Automation.PSCredential($mockAnalysisServiceAccount,($mockSQLServicePassword | ConvertTo-SecureString -AsPlainText -Force)) + $mockAnalysisServicePassword = 'Analysiss3v!c3P@ssw0rd' + $mockAnalysisServiceCredential = New-Object System.Management.Automation.PSCredential($mockAnalysisServiceAccount,($mockAnalysisServicePassword | ConvertTo-SecureString -AsPlainText -Force)) $mockClusterNodes = @($env:COMPUTERNAME,'SQL01','SQL02') @@ -608,22 +674,22 @@ try ) } - $mockStartProcessExpectedArgument = '' # Set dynamically during runtime - $mockStartProcessExitCode = 0 # Set dynamically during runtime + $mockStartSqlSetupProcessExpectedArgument = '' # Set dynamically during runtime + $mockStartSqlSetupProcessExitCode = 0 # Set dynamically during runtime - $mockStartProcess_Robocopy = { - if ( $ArgumentList -cne $mockStartProcessExpectedArgument ) + $mockStartSqlSetupProcess_Robocopy = { + if ( $ArgumentList -cne $mockStartSqlSetupProcessExpectedArgument ) { - throw "Expected arguments was not the same as the arguments in the function call.`nExpected: '$mockStartProcessExpectedArgument' `n But was: '$ArgumentList'" + throw "Expected arguments was not the same as the arguments in the function call.`nExpected: '$mockStartSqlSetupProcessExpectedArgument' `n But was: '$ArgumentList'" } return New-Object Object | Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value 0 -PassThru -Force } - $mockStartProcess_Robocopy_WithExitCode = { + $mockStartSqlSetupProcess_Robocopy_WithExitCode = { return New-Object Object | - Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value $mockStartProcessExitCode -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value $mockStartSqlSetupProcessExitCode -PassThru -Force } $mockSourcePathUNCWithoutLeaf = '\\server\share' @@ -825,116 +891,47 @@ try Needed a way to see into the Set-method for the arguments the Set-method is building and sending to 'setup.exe', and fail the test if the arguments is different from the expected arguments. Solved this by dynamically set the expected arguments before each It-block. If the arguments differs the mock of - Start-Process throws an error message, similar to what Pester would have reported (expected -> but was). + Start-SqlSetupProcess throws an error message, similar to what Pester would have reported (expected -> but was). #> - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} - $mockStartProcessExpectedArgumentClusterDefault = @{ + $mockStartSqlSetupProcessExpectedArgumentClusterDefault = @{ IAcceptSQLServerLicenseTerms = 'True' Quiet = 'True' InstanceName = 'MSSQLSERVER' Features = 'SQLENGINE' - SQLSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' FailoverClusterGroup = 'SQL Server (MSSQLSERVER)' } - $mockStartProcess_ParameterFilter = { - <# - The mock for Start-Process fakes a System.Diagnostics.Process using Start-Process. - We don't want to mock that. - #> - $FilePath -ne 'whoami' - } - - $mockStartProcess = { - $argumentHashTable = @{} - - # Break the argument string into a hash table - ($ArgumentList -split ' ?/') | ForEach-Object { - if ($_ -imatch '(\w+)="?([^/]+)"?') - { - $key = $Matches[1] - if ($key -in ('FailoverClusterDisks','FailoverClusterIPAddresses')) - { - $value = ($Matches[2] -replace '" "','; ') -replace '"','' - } - else - { - $value = ($Matches[2] -replace '" "',' ') -replace '"','' - } - - $null = $argumentHashTable.Add($key, $value) - } - } - - $actualValues = $argumentHashTable.Clone() + $mockStartSqlSetupProcess = { + Test-SetupArgument -Argument $ArgumentList -ExpectedArgument $mockStartSqlSetupProcessExpectedArgument - # Start by checking whether we have the same number of parameters - Write-Verbose 'Verifying setup argument count (expected vs actual)' -Verbose - Write-Verbose -Message ('Expected: {0}' -f ($mockStartProcessExpectedArgument.Keys -join ',') ) -Verbose - Write-Verbose -Message ('Actual: {0}' -f ($actualValues.Keys -join ',')) -Verbose - - $numberOfActualValues = $actualValues.Count - $numberOfExpectedValues = $mockStartProcessExpectedArgument.Count - - $numberOfActualValues | Should Be $numberOfExpectedValues - - Write-Verbose 'Verifying actual setup arguments against expected setup arguments' -Verbose - foreach ($argumentKey in $mockStartProcessExpectedArgument.Keys) - { - $argumentKeyName = $argumentHashTable.GetEnumerator() | Where-Object -FilterScript { $_.Name -eq $argumentKey } | Select-Object -ExpandProperty Name - $argumentKeyName | Should Be $argumentKey - - $argumentValue = $argumentHashTable.$argumentKey - $argumentValue | Should Be $mockStartProcessExpectedArgument.$argumentKey - } - - <# - Because Wait-Process expects the type System.Diagnostics.Process[], and the - type cannot be easily mocked. A try was made using New-Object to create an - dummy object, but since the property ExitCode is read only it was not - possible. So a workaround is to start a real process with a command that is - harmless ('whoami') which will populate ExitCode correctly. - Redirecting output to a temporary file so that the output (name) from whoami - is not visible in the console. - #> - $temporaryFileForOutput = New-TemporaryFile - $setupProcess = Start-Process -FilePath 'whoami' -PassThru -Wait -NoNewWindow -RedirectStandardOutput $temporaryFileForOutput.FullName - Remove-Item $temporaryFileForOutput.FullName -Force + return 0 + } - return $setupProcess + $mockDynamicSetupProcessExitCode = 0 + $mockStartSqlSetupProcess_WithDynamicExitCode = { + return $mockDynamicSetupProcessExitCode } #endregion Function mocks - $mockStartProcess_SetupCredential = { - $Credential | Should Not BeNullOrEmpty - - # See comment in script block for $mockStartProcess (didn't want to duplicate comment) - $temporaryFileForOutput = New-TemporaryFile - $setupProcess = Start-Process -FilePath 'whoami' -PassThru -Wait -NoNewWindow -RedirectStandardOutput $temporaryFileForOutput.FullName - Remove-Item $temporaryFileForOutput.FullName -Force - - return $setupProcess - } + <# + These are written with both lower-case and upper-case to make sure we support that. + The feature list must be written in the order it is returned by the function Get-TargetResource. + #> + $defaultFeatures = 'SQLEngine,Replication,Dqc,Dq,Bol,Conn,Bc,Sdk,FullText,Rs,As,Is,Ssms,Adv_Ssms' # Default parameters that are used for the It-blocks $mockDefaultParameters = @{ - SetupCredential = $mockSetupCredential - <# - These are written with both lower-case and upper-case to make sure we support that. - The feature list must be written in the order it is returned by the function Get-TargerResource. - #> - Features = 'SQLEngine,Replication,Dqc,Dq,Bol,Conn,Bc,Sdk,FullText,Rs,As,Is,Ssms,Adv_Ssms' + Features = $defaultFeatures } - $featuresForSqlServer2016 = '' - $mockDefaultClusterParameters = @{ - SetupCredential = $mockSetupCredential + SQLSysAdminAccounts = 'COMPANY\User1','COMPANY\SQLAdmins' - # Feature support is tested elsewhere, so just include the minimum + # Feature support is tested elsewhere, so just include the minimum. Features = 'SQLEngine' - } Describe "xSQLServerSetup\Get-TargetResource" -Tag 'Get' { @@ -949,7 +946,7 @@ try #endregion Setting up TestDrive:\ - BeforeEach { + BeforeAll { # General mocks Mock -CommandName Get-SqlMajorVersion -MockWith $mockGetSqlMajorVersion -Verifiable Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable @@ -993,6 +990,11 @@ try Mock -CommandName Get-ItemProperty -ParameterFilter { $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$($mockSqlMajorVersion)0\Tools\Setup\Client_Components_Full" } -MockWith $mockGetItemProperty_ClientComponentsFull_FeatureList -Verifiable + + } + + BeforeEach { + $testParameters = $mockDefaultParameters.Clone() } $testProductVersion | ForEach-Object -Process { @@ -1009,7 +1011,6 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for default instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName @@ -1132,7 +1133,6 @@ try Context "When using SourceCredential parameter and SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for default instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName @@ -1255,7 +1255,6 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for features 'CONN', 'SDK' and 'BC'" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName @@ -1364,7 +1363,6 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is in the desired state for default instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName @@ -1576,7 +1574,6 @@ try Context "When using SourceCredential parameter and SQL Server version is $mockSqlMajorVersion and the system is in the desired state for default instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName @@ -1797,7 +1794,6 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for named instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockNamedInstance_InstanceName @@ -1916,7 +1912,6 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is in the desired state for named instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters.Remove('Features') $testParameters += @{ InstanceName = $mockNamedInstance_InstanceName @@ -2123,11 +2118,10 @@ try } Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for a clustered default instance" { - - BeforeAll { - $testParams = $mockDefaultParameters.Clone() - $testParams.Remove('Features') - $testParams += @{ + BeforeEach { + $testParameters = $mockDefaultParameters.Clone() + $testParameters.Remove('Features') + $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null SourcePath = $mockSourcePath @@ -2145,8 +2139,7 @@ try } It 'Should not attempt to collect cluster information for a standalone instance' { - - $currentState = Get-TargetResource @testParams + $currentState = Get-TargetResource @testParameters Assert-MockCalled -CommandName Connect-SQL -Exactly -Times 0 -Scope It Assert-MockCalled -CommandName Get-CimInstance -Exactly -Times 0 -Scope It @@ -2161,9 +2154,9 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is in the desired state for a clustered default instance" { BeforeEach { - $testParams = $mockDefaultParameters.Clone() - $testParams.Remove('Features') - $testParams += @{ + $testParameters = $mockDefaultParameters.Clone() + $testParameters.Remove('Features') + $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null SourcePath = $mockSourcePath @@ -2187,18 +2180,18 @@ try } It 'Should collect information for a clustered instance' { - $currentState = Get-TargetResource @testParams + $currentState = Get-TargetResource @testParameters Assert-MockCalled -CommandName Connect-SQL -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Get-CimInstance -Exactly -Times 1 -Scope It -ParameterFilter { $Filter -eq "Type = 'SQL Server'" } Assert-MockCalled -CommandName Get-CimAssociatedInstance -Exactly -Times 1 -Scope It -ParameterFilter { $ResultClassName -eq 'MSCluster_ResourceGroup' } Assert-MockCalled -CommandName Get-CimAssociatedInstance -Exactly -Times 2 -Scope It -ParameterFilter { $ResultClassName -eq 'MSCluster_Resource' } - $currentState.InstanceName | Should Be $testParams.InstanceName + $currentState.InstanceName | Should Be $testParameters.InstanceName } It 'Should return correct cluster information' { - $currentState = Get-TargetResource @testParams + $currentState = Get-TargetResource @testParameters $currentState.FailoverClusterGroupName | Should Be $mockDefaultInstance_FailoverClusterGroupName $currentState.FailoverClusterIPAddress | Should Be $mockDefaultInstance_FailoverClusterIPAddress @@ -2222,7 +2215,7 @@ try #endregion Setting up TestDrive:\ - BeforeEach { + BeforeAll { # General mocks Mock -CommandName Get-SqlMajorVersion -MockWith $mockGetSqlMajorVersion -Verifiable Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable @@ -2876,36 +2869,37 @@ try #endregion Setting up TestDrive:\ - BeforeEach { + BeforeAll { # General mocks Mock -CommandName Get-SqlMajorVersion -MockWith $mockGetSqlMajorVersion -Verifiable - Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable - Mock -CommandName Connect-SQLAnalysis -MockWith $mockConnectSQLAnalysis -Verifiable - Mock -CommandName Get-ItemProperty -ParameterFilter { - $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -and - ($Name -eq $mockDefaultInstance_InstanceName -or $Name -eq $mockNamedInstance_InstanceName) - } -MockWith $mockGetItemProperty_SQL -Verifiable - - Mock -CommandName Get-ItemProperty -ParameterFilter { - ( - $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$mockDefaultInstance_AnalysisServiceName" -or - $Path -eq "HKLM:\SYSTEM\CurrentControlSet\Services\$mockNamedInstance_AnalysisServiceName" - ) -and - $Name -eq 'ImagePath' - } -MockWith $mockGetItemProperty_ServicesAnalysis -Verifiable # Mocking SharedDirectory and SharedWowDirectory (when not previously installed) Mock -CommandName Get-ItemProperty -Verifiable - Mock -CommandName Get-ItemProperty -ParameterFilter { - $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$($mockSqlMajorVersion)0\Tools\Setup\Client_Components_Full" - } -MockWith $mockGetItemProperty_ClientComponentsFull_FeatureList -Verifiable - - Mock -CommandName Start-Process -MockWith $mockStartProcess -ParameterFilter $mockStartProcess_ParameterFilter -Verifiable - Mock -CommandName Wait-Process -Verifiable + Mock -CommandName Start-SqlSetupProcess -MockWith $mockStartSqlSetupProcess -Verifiable Mock -CommandName Test-TargetResource -MockWith { return $true } -Verifiable + + <#1 + These mock should not have Verifiable because they are used to test so we never + call them in Assert-MockCalled. + #> + Mock -CommandName Connect-SQL -MockWith $mockConnectSQL + Mock -CommandName Connect-SQLAnalysis -MockWith $mockConnectSQLAnalysis + + # Mock PsDscRunAsCredential context. + $PsDscContext = @{ + RunAsUser = $mockSetupCredential.UserName + } + } + + BeforeEach { + $testParameters = $mockDefaultParameters.Clone() + $testParameters += @{ + SQLSysAdminAccounts = 'COMPANY\User1','COMPANY\SQLAdmins' + ASSysAdminAccounts = 'COMPANY\User1','COMPANY\SQLAdmins' + } } $testProductVersion | ForEach-Object -Process { @@ -2920,6 +2914,57 @@ try $mockSqlDefaultDatabaseFilePath = "C:\Program Files\Microsoft SQL Server\$($mockDefaultInstance_InstanceId)\MSSQL\DATA\" $mockSqlDefaultDatabaseLogPath = "C:\Program Files\Microsoft SQL Server\$($mockDefaultInstance_InstanceId)\MSSQL\DATA\" + Context "When setup process fails with exit code " { + BeforeEach { + Mock -CommandName Start-SqlSetupProcess -MockWith $mockStartSqlSetupProcess_WithDynamicExitCode -Verifiable + Mock -CommandName New-SmbMapping -Verifiable + Mock -CommandName Remove-SmbMapping -Verifiable + Mock -CommandName Copy-ItemWithRobocopy -Verifiable + Mock -CommandName Get-TemporaryFolder -MockWith $mockGetTemporaryFolder -Verifiable + Mock -CommandName New-Guid -MockWith $mockNewGuid -Verifiable + Mock -CommandName Get-Service -MockWith $mockEmptyHashtable -Verifiable + + Mock -CommandName Get-ItemProperty -ParameterFilter { + $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudio2008R2_ProductIdentifyingNumber) -or + $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudio2012_ProductIdentifyingNumber) -or + $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudio2014_ProductIdentifyingNumber) -or + $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2008R2_ProductIdentifyingNumber) -or + $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2012_ProductIdentifyingNumber) -or + $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) + } -MockWith $mockEmptyHashtable -Verifiable + + Mock -CommandName Get-CimInstance -MockWith $mockEmptyHashtable -Verifiable + + + $testParameters += @{ + InstanceName = $mockDefaultInstance_InstanceName + SourceCredential = $null + SourcePath = $mockSourcePath + } + $testParameters.Features = 'SQLENGINE' + } + + Context 'When exit code is 3010' { + $mockDynamicSetupProcessExitCode = 3010 + + Mock -CommandName Write-Warning + + It 'Should warn that target nod need to restart' { + { Set-TargetResource @testParameters } | Should Not Throw + + Assert-MockCalled -CommandName Write-Warning -Exactly -Times 1 -Scope It + } + } + + Context 'When exit code is any other (exit code is set to 1 for the test)' { + $mockDynamicSetupProcessExitCode = 1 + + It 'Should throw the correct error message' { + { Set-TargetResource @testParameters } | Should Throw 'Setup exited with code '1'. Please see the ''Summary.txt'' log file in the ''Setup Bootstrap\Log'' folder.' + } + } + } + Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for a default instance" { BeforeEach { Mock -CommandName New-SmbMapping -Verifiable @@ -2942,14 +2987,11 @@ try } It 'Should set the system in the desired state when feature is SQLENGINE' { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null SourcePath = $mockSourcePath ProductKey = '1FAKE-2FAKE-3FAKE-4FAKE-5FAKE' - SQLSysAdminAccounts = 'COMPANY\User1','COMPANY\SQLAdmins' - ASSysAdminAccounts = 'COMPANY\User1','COMPANY\SQLAdmins' InstanceDir = 'D:' InstallSQLDataDir = 'E:' InstallSharedDir = 'C:\Program Files\Microsoft SQL Server' @@ -2961,7 +3003,7 @@ try $testParameters.Features = $testParameters.Features -replace ',SSMS,ADV_SSMS','' } - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3002,15 +3044,13 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } if( $mockSqlMajorVersion -in (13,14) ) { It 'Should throw when feature parameter contains ''SSMS'' when installing SQL Server 2016' { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null @@ -3018,13 +3058,12 @@ try } $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} { Set-TargetResource @testParameters } | Should Throw "'SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } It 'Should throw when feature parameter contains ''ADV_SSMS'' when installing SQL Server 2016' { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null @@ -3032,7 +3071,7 @@ try } $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} { Set-TargetResource @testParameters } | Should Throw "'ADV_SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } @@ -3040,7 +3079,6 @@ try else { It 'Should set the system in the desired state when feature is SSMS' { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null @@ -3049,7 +3087,7 @@ try $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3077,13 +3115,11 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } It 'Should set the system in the desired state when feature is ADV_SSMS' { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $null @@ -3092,7 +3128,7 @@ try $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3121,8 +3157,7 @@ try } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } } @@ -3130,7 +3165,6 @@ try Context "When using SourceCredential parameter, and using a UNC path with a leaf, and SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for a default instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $mockSetupCredential @@ -3174,16 +3208,15 @@ try } It 'Should set the system in the desired state when feature is SQLENGINE' { - - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' AgtSvcStartupType = 'Automatic' InstanceName = 'MSSQLSERVER' Features = $testParameters.Features - SQLSysAdminAccounts = 'COMPANY\sqladmin' - ASSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' + ASSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' } { Set-TargetResource @testParameters } | Should Not Throw @@ -3212,8 +3245,7 @@ try } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } @@ -3221,14 +3253,14 @@ try { It 'Should throw when feature parameter contains ''SSMS'' when installing SQL Server 2016' { $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = '' + $mockStartSqlSetupProcessExpectedArgument = '' { Set-TargetResource @testParameters } | Should Throw "'SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } It 'Should throw when feature parameter contains ''ADV_SSMS'' when installing SQL Server 2016' { $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = '' + $mockStartSqlSetupProcessExpectedArgument = '' { Set-TargetResource @testParameters } | Should Throw "'ADV_SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } @@ -3238,7 +3270,7 @@ try It 'Should set the system in the desired state when feature is SSMS' { $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3267,15 +3299,14 @@ try } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } It 'Should set the system in the desired state when feature is ADV_SSMS' { $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3302,8 +3333,7 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } } @@ -3311,7 +3341,6 @@ try Context "When using SourceCredential parameter, and using a UNC path without a leaf, and SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for a default instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockDefaultInstance_InstanceName SourceCredential = $mockSetupCredential @@ -3344,15 +3373,15 @@ try } It 'Should set the system in the desired state when feature is SQLENGINE' { - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' AGTSVCSTARTUPTYPE = 'Automatic' InstanceName = 'MSSQLSERVER' Features = $testParameters.Features - SQLSysAdminAccounts = 'COMPANY\sqladmin' - ASSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' + ASSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' } { Set-TargetResource @testParameters } | Should Not Throw @@ -3381,8 +3410,7 @@ try } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } @@ -3390,14 +3418,14 @@ try { It 'Should throw when feature parameter contains ''SSMS'' when installing SQL Server 2016' { $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} { Set-TargetResource @testParameters } | Should Throw "'SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } It 'Should throw when feature parameter contains ''ADV_SSMS'' when installing SQL Server 2016' { $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} { Set-TargetResource @testParameters } | Should Throw "'ADV_SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } @@ -3407,7 +3435,7 @@ try It 'Should set the system in the desired state when feature is SSMS' { $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3436,15 +3464,14 @@ try } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } It 'Should set the system in the desired state when feature is ADV_SSMS' { $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3471,8 +3498,7 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } } @@ -3489,7 +3515,6 @@ try Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state for a named instance" { BeforeEach { - $testParameters = $mockDefaultParameters.Clone() $testParameters += @{ InstanceName = $mockNamedInstance_InstanceName SourceCredential = $null @@ -3516,15 +3541,15 @@ try } It 'Should set the system in the desired state when feature is SQLENGINE' { - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' AGTSVCSTARTUPTYPE = 'Automatic' InstanceName = 'TEST' Features = $testParameters.Features - SQLSysAdminAccounts = 'COMPANY\sqladmin' - ASSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' + ASSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' } { Set-TargetResource @testParameters } | Should Not Throw @@ -3548,8 +3573,7 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } @@ -3557,14 +3581,14 @@ try { It 'Should throw when feature parameter contains ''SSMS'' when installing SQL Server 2016' { $testParameters.Features = $($testParameters.Features), 'SSMS' -join ',' - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} { Set-TargetResource @testParameters } | Should Throw "'SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } It 'Should throw when feature parameter contains ''ADV_SSMS'' when installing SQL Server 2016' { $testParameters.Features = $($testParameters.Features), 'ADV_SSMS' -join ',' - $mockStartProcessExpectedArgument = @{} + $mockStartSqlSetupProcessExpectedArgument = @{} { Set-TargetResource @testParameters } | Should Throw "'ADV_SSMS' is not a valid value for setting 'FEATURES'. Refer to SQL Help for more information." } @@ -3574,7 +3598,7 @@ try It 'Should set the system in the desired state when feature is SSMS' { $testParameters.Features = 'SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3602,15 +3626,14 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } It 'Should set the system in the desired state when feature is ADV_SSMS' { $testParameters.Features = 'ADV_SSMS' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ Quiet = 'True' IAcceptSQLServerLicenseTerms = 'True' Action = 'Install' @@ -3637,8 +3660,7 @@ try $Path -eq (Join-Path -Path $mockRegistryUninstallProductsPath -ChildPath $mockSqlServerManagementStudioAdvanced2014_ProductIdentifyingNumber) } -Exactly -Times 6 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It } } @@ -3646,7 +3668,7 @@ try # For testing AddNode action Context "When SQL Server version is $mockSQLMajorVersion and the system is not in the desired state and the action is AddNode" { - BeforeAll { + BeforeEach { $testParameters = $mockDefaultClusterParameters.Clone() $testParameters['Features'] += 'AS' $testParameters += @{ @@ -3665,7 +3687,7 @@ try Mock -CommandName Get-CimAssociatedInstance -MockWith $mockGetCimAssociatedInstance_MSCluster_ResourceGroupToResource -ParameterFilter { ($Association -eq 'MSCluster_ResourceGroupToResource') -and ($ResultClassName -eq 'MSCluster_Resource') - } -Verfiable + } -Verifiable Mock -CommandName Get-CimAssociatedInstance -MockWith $mockGetCimAssociatedInstance_MSCluster_ResourceToPossibleOwner -ParameterFilter { $Association -eq 'MSCluster_ResourceToPossibleOwner' @@ -3692,7 +3714,7 @@ try } It 'Should pass proper parameters to setup' { - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ IAcceptSQLServerLicenseTerms = 'True' Quiet = 'True' Action = 'AddNode' @@ -3708,17 +3730,11 @@ try { Set-TargetResource @testParameters } | Should Not Throw } - - It 'Should pass the SetupCredential object to the Start-Process function' { - Mock -CommandName Start-Process -MockWith $mockStartProcess_SetupCredential -ParameterFilter $mockStartProcess_ParameterFilter -Verifiable - - { Set-TargetResource @testParameters } | Should Not Throw - } } # For testing InstallFailoverCluster action Context "When SQL Server version is $mockSQLMajorVersion and the system is not in the desired state and the action is InstallFailoverCluster" { - BeforeAll { + BeforeEach { $mockDynamicSqlDataDirectoryPath = $mockSqlDataDirectoryPath $mockDynamicSqlUserDatabasePath = $mockSqlUserDatabasePath $mockDynamicSqlUserDatabaseLogPath = $mockSqlUserDatabaseLogPath @@ -3793,8 +3809,8 @@ try } It 'Should pass proper parameters to setup' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'InstallFailoverCluster' FailoverClusterDisks = 'Backup; SysData; TempDbData; TempDbLogs; UserData; UserLogs' FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite @@ -3812,8 +3828,8 @@ try } It 'Should pass proper parameters to setup when only InstallSQLDataDir is assigned a path' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'InstallFailoverCluster' FailoverClusterDisks = 'SysData' FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite @@ -3833,8 +3849,8 @@ try } It 'Should pass proper parameters to setup when three variables are assigned the same drive, but different paths' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'InstallFailoverCluster' FailoverClusterDisks = 'SysData' FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite @@ -3857,12 +3873,6 @@ try { Set-TargetResource @setTargetResourceParameters } | Should Not Throw } - It 'Should pass the SetupCredential object to the Start-Process function' { - Mock -CommandName Start-Process -MockWith $mockStartProcess_SetupCredential -ParameterFilter $mockStartProcess_ParameterFilter -Verifiable - - { Set-TargetResource @testParameters } | Should Not Throw - } - It 'Should throw an error when one or more paths are not resolved to clustered storage' { $badPathParameters = $testParameters.Clone() @@ -3873,8 +3883,8 @@ try } It 'Should properly map paths to clustered disk resources' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'InstallFailoverCluster' FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite InstallSQLDataDir = $mockDynamicSqlDataDirectoryPath @@ -3892,11 +3902,11 @@ try } It 'Should build a DEFAULT address string when no network is specified' { - $missingNetworkParams = $testParameters.Clone() - $missingNetworkParams.Remove('FailoverClusterIPAddress') + $missingNetworkParameters = $testParameters.Clone() + $missingNetworkParameters.Remove('FailoverClusterIPAddress') - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'InstallFailoverCluster' FailoverClusterIPAddresses = 'DEFAULT' FailoverClusterNetworkName = $mockDefaultInstance_FailoverClusterNetworkName @@ -3910,7 +3920,7 @@ try SkipRules = 'Cluster_VerifyForErrors' } - { Set-TargetResource @missingNetworkParams } | Should Not Throw + { Set-TargetResource @missingNetworkParameters } | Should Not Throw } It 'Should throw an error when an invalid IP Address is specified' { @@ -3937,8 +3947,8 @@ try It 'Should build a valid IP address string for a single address' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite FailoverClusterNetworkName = $mockDefaultInstance_FailoverClusterNetworkName InstallSQLDataDir = $mockDynamicSqlDataDirectoryPath @@ -3962,8 +3972,8 @@ try FailoverClusterIPAddress = ($mockClusterSites | ForEach-Object { $_.Address }) } - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_MultiSite FailoverClusterNetworkName = $mockDefaultInstance_FailoverClusterNetworkName InstallSQLDataDir = $mockDynamicSqlDataDirectoryPath @@ -3990,11 +4000,11 @@ try $csvTestParameters['SQLTempDBLogDir'] = $mockCSVClusterDiskMap['TempDBLogs'].Path $csvTestParameters['SQLBackupDir'] = $mockCSVClusterDiskMap['Backup'].Path - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ IAcceptSQLServerLicenseTerms = 'True' SkipRules = 'Cluster_VerifyForErrors' Quiet = 'True' - SQLSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' Action = 'InstallFailoverCluster' InstanceName = 'MSSQLSERVER' Features = 'SQLEngine' @@ -4023,11 +4033,11 @@ try $csvTestParameters['SQLTempDBLogDir'] = $mockCSVClusterDiskMap['UserData'].Path + '\TEMPDBLOG' $csvTestParameters['SQLBackupDir'] = $mockCSVClusterDiskMap['Backup'].Path + '\Backup' - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ IAcceptSQLServerLicenseTerms = 'True' SkipRules = 'Cluster_VerifyForErrors' Quiet = 'True' - SQLSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' Action = 'InstallFailoverCluster' InstanceName = 'MSSQLSERVER' Features = 'SQLEngine' @@ -4048,8 +4058,7 @@ try } Context "When SQL Server version is $mockSqlMajorVersion and the system is not in the desired state and the action is PrepareFailoverCluster" { - BeforeAll { - $testParameters = $mockDefaultParameters.Clone() + BeforeEach { $testParameters.Remove('Features') $testParameters.Remove('SourceCredential') $testParameters.Remove('ASSysAdminAccounts') @@ -4083,7 +4092,7 @@ try $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$mockDefaultInstance_InstanceId\Setup" -and $Name -eq 'SqlProgramDir' } -MockWith $mockGetItemProperty_Setup -Verifiable - Mock -CommandName Start-Process -MockWith $mockStartProcess -ParameterFilter $mockStartProcess_ParameterFilter -Verifiable + Mock -CommandName Start-SqlSetupProcess -MockWith $mockStartSqlSetupProcess -Verifiable Mock -CommandName Get-CimInstance -MockWith {} -ParameterFilter { ($Namespace -eq 'root/MSCluster') -and ($ClassName -eq 'MSCluster_ResourceGroup') -and ($Filter -eq "Name = 'Available Storage'") @@ -4107,14 +4116,13 @@ try } It 'Should pass correct arguments to the setup process' { - - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'PrepareFailoverCluster' SkipRules = 'Cluster_VerifyForErrors' } - $mockStartProcessExpectedArgument.Remove('FailoverClusterGroup') - $mockStartProcessExpectedArgument.Remove('SQLSysAdminAccounts') + $mockStartSqlSetupProcessExpectedArgument.Remove('FailoverClusterGroup') + $mockStartSqlSetupProcessExpectedArgument.Remove('SQLSysAdminAccounts') { Set-TargetResource @testParameters } | Should Not throw @@ -4125,8 +4133,7 @@ try $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' -and ($Name -eq $mockDefaultInstance_InstanceName) } -Exactly -Times 0 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Wait-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-SqlSetupProcess -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Test-TargetResource -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Get-CimInstance -ParameterFilter { @@ -4177,9 +4184,7 @@ try SQLTempDbLogDir = $mockDynamicSqlTempDatabaseLogPath SQLBackupDir = $mockDynamicSqlBackupPath } - } - BeforeAll { Mock -CommandName Get-CimInstance -MockWith $mockGetCimInstance_MSClusterResourceGroup_AvailableStorage -ParameterFilter { $Filter -eq "Name = 'Available Storage'" } -Verifiable @@ -4222,8 +4227,8 @@ try It 'Should properly map paths to clustered disk resources' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'CompleteFailoverCluster' FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite InstallSQLDataDir = $mockDynamicSqlDataDirectoryPath @@ -4241,11 +4246,11 @@ try } It 'Should build a DEFAULT address string when no network is specified' { - $missingNetworkParams = $testParameters.Clone() - $missingNetworkParams.Remove('FailoverClusterIPAddress') + $missingNetworkParameters = $testParameters.Clone() + $missingNetworkParameters.Remove('FailoverClusterIPAddress') - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ Action = 'CompleteFailoverCluster' FailoverClusterIPAddresses = 'DEFAULT' FailoverClusterNetworkName = $mockDefaultInstance_FailoverClusterNetworkName @@ -4259,7 +4264,7 @@ try SkipRules = 'Cluster_VerifyForErrors' } - { Set-TargetResource @missingNetworkParams } | Should Not Throw + { Set-TargetResource @missingNetworkParameters } | Should Not Throw } It 'Should throw an error when an invalid IP Address is specified' { @@ -4286,8 +4291,8 @@ try It 'Should build a valid IP address string for a single address' { - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_SingleSite FailoverClusterNetworkName = $mockDefaultInstance_FailoverClusterNetworkName InstallSQLDataDir = $mockDynamicSqlDataDirectoryPath @@ -4311,8 +4316,8 @@ try FailoverClusterIPAddress = ($mockClusterSites | ForEach-Object { $_.Address }) } - $mockStartProcessExpectedArgument = $mockStartProcessExpectedArgumentClusterDefault.Clone() - $mockStartProcessExpectedArgument += @{ + $mockStartSqlSetupProcessExpectedArgument = $mockStartSqlSetupProcessExpectedArgumentClusterDefault.Clone() + $mockStartSqlSetupProcessExpectedArgument += @{ FailoverClusterIPAddresses = $mockDefaultInstance_FailoverClusterIPAddressParameter_MultiSite FailoverClusterNetworkName = $mockDefaultInstance_FailoverClusterNetworkName InstallSQLDataDir = $mockDynamicSqlDataDirectoryPath @@ -4330,11 +4335,11 @@ try } It 'Should pass proper parameters to setup' { - $mockStartProcessExpectedArgument = @{ + $mockStartSqlSetupProcessExpectedArgument = @{ IAcceptSQLServerLicenseTerms = 'True' SkipRules = 'Cluster_VerifyForErrors' Quiet = 'True' - SQLSysAdminAccounts = 'COMPANY\sqladmin' + SQLSysAdminAccounts = 'COMPANY\sqladmin COMPANY\SQLAdmins COMPANY\User1' Action = 'CompleteFailoverCluster' InstanceName = 'MSSQLSERVER' @@ -4365,14 +4370,14 @@ try Context 'When Copy-ItemWithRobocopy is called it should return the correct arguments' { BeforeEach { Mock -CommandName Get-Command -MockWith $mockGetCommand -Verifiable - Mock -CommandName Start-Process -MockWith $mockStartProcess_Robocopy -Verifiable + Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy -Verifiable } It 'Should use Unbuffered IO when copying' { $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO - $mockStartProcessExpectedArgument = + $mockStartSqlSetupProcessExpectedArgument = $mockRobocopyArgumentSourcePath, $mockRobocopyArgumentDestinationPath, $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty, @@ -4380,21 +4385,21 @@ try $mockRobocopyArgumentUseUnbufferedIO, $mockRobocopyArgumentSilent -join ' ' - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Not Throw + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Not Throw Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It } It 'Should not use Unbuffered IO when copying' { $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithoutUnbufferedIO - $mockStartProcessExpectedArgument = + $mockStartSqlSetupProcessExpectedArgument = $mockRobocopyArgumentSourcePath, $mockRobocopyArgumentDestinationPath, $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty, @@ -4402,12 +4407,12 @@ try '', $mockRobocopyArgumentSilent -join ' ' - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Not Throw + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Not Throw Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It @@ -4419,46 +4424,46 @@ try $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO Mock -CommandName Get-Command -MockWith $mockGetCommand -Verifiable - Mock -CommandName Start-Process -MockWith $mockStartProcess_Robocopy_WithExitCode -Verifiable + Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy_WithExitCode -Verifiable } It 'Should throw the correct error message when error code is 8' { - $mockStartProcessExitCode = 8 + $mockStartSqlSetupProcessExitCode = 8 - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Throw "Robocopy reported errors when copying files. Error code: $mockStartProcessExitCode." + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Throw "Robocopy reported errors when copying files. Error code: $mockStartSqlSetupProcessExitCode." Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It } It 'Should throw the correct error message when error code is 16' { - $mockStartProcessExitCode = 16 + $mockStartSqlSetupProcessExitCode = 16 - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Throw "Robocopy reported errors when copying files. Error code: $mockStartProcessExitCode." + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Throw "Robocopy reported errors when copying files. Error code: $mockStartSqlSetupProcessExitCode." Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It } It 'Should throw the correct error message when error code is greater than 7 (but not 8 or 16)' { - $mockStartProcessExitCode = 9 + $mockStartSqlSetupProcessExitCode = 9 - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Throw "Robocopy reported that failures occured when copying files. Error code: $mockStartProcessExitCode." + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Throw "Robocopy reported that failures occurred when copying files. Error code: $mockStartSqlSetupProcessExitCode." Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It @@ -4470,49 +4475,51 @@ try $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO Mock -CommandName Get-Command -MockWith $mockGetCommand -Verifiable - Mock -CommandName Start-Process -MockWith $mockStartProcess_Robocopy_WithExitCode -Verifiable + Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy_WithExitCode -Verifiable + } + + AfterEach { + Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It + Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It } It 'Should finish successfully with exit code 1' { - $mockStartProcessExitCode = 1 + $mockStartSqlSetupProcessExitCode = 1 - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Not Throw + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Not Throw Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It } It 'Should finish successfully with exit code 2' { - $mockStartProcessExitCode = 2 + $mockStartSqlSetupProcessExitCode = 2 - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Not Throw + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Not Throw Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It } It 'Should finish successfully with exit code 3' { - $mockStartProcessExitCode = 3 + $mockStartSqlSetupProcessExitCode = 3 - $copyItemWithRoboCopyParameter = @{ + $copyItemWithRobocopyParameter = @{ Path = $mockRobocopyArgumentSourcePath DestinationPath = $mockRobocopyArgumentDestinationPath } - { Copy-ItemWithRobocopy @copyItemWithRoboCopyParameter } | Should Not Throw - - Assert-MockCalled -CommandName Get-Command -Exactly -Times 1 -Scope It - Assert-MockCalled -CommandName Start-Process -Exactly -Times 1 -Scope It + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should Not Throw } } } @@ -4582,12 +4589,39 @@ try $mockExpectedTempPath = [IO.Path]::GetTempPath() } - Context "When using Get-TemporaryFolder" { - It "Should return the correct temporary path." { + Context 'When using Get-TemporaryFolder' { + It 'Should return the correct temporary path.' { Get-TemporaryFolder | Should BeExactly $mockExpectedTempPath } } } + + Describe 'Start-SqlSetupProcess' -Tag 'Helper' { + Context 'When starting a process successfully' { + It 'Should return exit code 0' { + $startSqlSetupProcessParameters = @{ + FilePath = 'powershell.exe' + ArgumentList = '-Command &{Start-Sleep -Seconds 2}' + Timeout = 30 + } + + $processExitCode = Start-SqlSetupProcess @startSqlSetupProcessParameters + $processExitCode | Should BeExactly 0 + } + } + + Context 'When starting a process and the process does not finish before the timeout period' { + It 'Should throw an error message' { + $startSqlSetupProcessParameters = @{ + FilePath = 'powershell.exe' + ArgumentList = '-Command &{Start-Sleep -Seconds 3}' + Timeout = 2 + } + + { Start-SqlSetupProcess @startSqlSetupProcessParameters } | Should Throw + } + } + } } } finally