diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd294027..8966c3481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- SqlServerDsc.Common + - Moved functions into individual files and use ModuleBuilder to assemble. + - SqlServerDsc - Refactor integration tests for _SQL Server Reporting Services_ and _Power BI_ _Report Server_ ([issue #2431](https://github.com/dsccommunity/SqlServerDsc/issues/2431)). diff --git a/build.yaml b/build.yaml index 7025a6dc6..5e7a6455f 100644 --- a/build.yaml +++ b/build.yaml @@ -69,7 +69,6 @@ BuildWorkflow: CopyPaths: - DSCResources - en-US - - Modules Prefix: prefix.ps1 Suffix: suffix.ps1 Encoding: UTF8 @@ -113,6 +112,13 @@ NestedModule: AddToManifest: false Exclude: PSGetModuleInfo.xml + SqlServerDsc.Common: + Prefix: prefix.ps1 + VersionedOutputDirectory: false + CopyPaths: + - en-US + Encoding: UTF8 + #################################################### # Pester Configuration (Sampler) # #################################################### diff --git a/source/Modules/SqlServerDsc.Common/Public/Connect-Sql.ps1 b/source/Modules/SqlServerDsc.Common/Public/Connect-Sql.ps1 new file mode 100644 index 000000000..c17056ae8 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Connect-Sql.ps1 @@ -0,0 +1,281 @@ +<# + .SYNOPSIS + Connect to a SQL Server Database Engine and return the server object. + + .PARAMETER ServerName + String containing the host name of the SQL Server to connect to. + Default value is the current computer name. + + .PARAMETER InstanceName + String containing the SQL Server Database Engine instance to connect to. + Default value is 'MSSQLSERVER'. + + .PARAMETER SetupCredential + The credentials to use to impersonate a user when connecting to the + SQL Server Database Engine instance. If this parameter is left out, then + the current user will be used to connect to the SQL Server Database Engine + instance using Windows Integrated authentication. + + .PARAMETER LoginType + Specifies which type of logon credential should be used. The valid types + are 'WindowsUser' or 'SqlLogin'. Default value is 'WindowsUser' + If set to 'WindowsUser' then the it will impersonate using the Windows + login specified in the parameter SetupCredential. + If set to 'WindowsUser' then the it will impersonate using the native SQL + login specified in the parameter SetupCredential. + + .PARAMETER StatementTimeout + Set the query StatementTimeout in seconds. Default 600 seconds (10 minutes). + + .PARAMETER Encrypt + Specifies if encryption should be used. + + .EXAMPLE + Connect-Sql + + Connects to the default instance on the local server. + + .EXAMPLE + Connect-Sql -InstanceName 'MyInstance' + + Connects to the instance 'MyInstance' on the local server. + + .EXAMPLE + Connect-Sql ServerName 'sql.company.local' -InstanceName 'MyInstance' -ErrorAction 'Stop' + + Connects to the instance 'MyInstance' on the server 'sql.company.local'. +#> +function Connect-Sql +{ + [CmdletBinding(DefaultParameterSetName = 'SqlServer')] + param + ( + [Parameter(ParameterSetName = 'SqlServer')] + [Parameter(ParameterSetName = 'SqlServerWithCredential')] + [ValidateNotNull()] + [System.String] + $ServerName = (Get-ComputerName), + + [Parameter(ParameterSetName = 'SqlServer')] + [Parameter(ParameterSetName = 'SqlServerWithCredential')] + [ValidateNotNull()] + [System.String] + $InstanceName = 'MSSQLSERVER', + + [Parameter(ParameterSetName = 'SqlServerWithCredential', Mandatory = $true)] + [ValidateNotNull()] + [Alias('SetupCredential', 'DatabaseCredential')] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter(ParameterSetName = 'SqlServerWithCredential')] + [ValidateSet('WindowsUser', 'SqlLogin')] + [System.String] + $LoginType = 'WindowsUser', + + [Parameter()] + [ValidateSet('tcp', 'np', 'lpc')] + [System.String] + $Protocol, + + [Parameter()] + [System.UInt16] + $Port, + + [Parameter()] + [ValidateNotNull()] + [System.Int32] + $StatementTimeout = 600, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Encrypt + ) + + Import-SqlDscPreferredModule + + <# + Build the connection string in the format: [protocol:]hostname[\instance][,port] + Examples: + - ServerName (default instance, no protocol/port) + - ServerName\Instance (named instance) + - tcp:ServerName (default instance with protocol) + - tcp:ServerName\Instance (named instance with protocol) + - ServerName,1433 (default instance with port) + - ServerName\Instance,50200 (named instance with port) + - tcp:ServerName,1433 (default instance with protocol and port) + - tcp:ServerName\Instance,50200 (named instance with protocol and port) + #> + if ($InstanceName -eq 'MSSQLSERVER') + { + $databaseEngineInstance = $ServerName + } + else + { + $databaseEngineInstance = '{0}\{1}' -f $ServerName, $InstanceName + } + + # Append port if specified + if ($PSBoundParameters.ContainsKey('Port')) + { + $databaseEngineInstance = '{0},{1}' -f $databaseEngineInstance, $Port + } + + # Prepend protocol if specified + if ($PSBoundParameters.ContainsKey('Protocol')) + { + $databaseEngineInstance = '{0}:{1}' -f $Protocol, $databaseEngineInstance + } + + $sqlServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $sqlConnectionContext = $sqlServerObject.ConnectionContext + $sqlConnectionContext.ServerInstance = $databaseEngineInstance + $sqlConnectionContext.StatementTimeout = $StatementTimeout + $sqlConnectionContext.ConnectTimeout = $StatementTimeout + $sqlConnectionContext.ApplicationName = 'SqlServerDsc' + + if ($Encrypt.IsPresent) + { + $sqlConnectionContext.EncryptConnection = $true + } + + if ($PSCmdlet.ParameterSetName -eq 'SqlServer') + { + <# + This is only used for verbose messaging and not for the connection + string since this is using Integrated Security=true (SSPI). + #> + $connectUserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + + Write-Verbose -Message ( + $script:localizedData.ConnectingUsingIntegrated -f $connectUsername + ) + } + else + { + $connectUserName = $Credential.UserName + + Write-Verbose -Message ( + $script:localizedData.ConnectingUsingImpersonation -f $connectUsername, $LoginType + ) + + if ($LoginType -eq 'SqlLogin') + { + $sqlConnectionContext.LoginSecure = $false + $sqlConnectionContext.Login = $connectUserName + $sqlConnectionContext.SecurePassword = $Credential.Password + } + + if ($LoginType -eq 'WindowsUser') + { + $sqlConnectionContext.LoginSecure = $true + $sqlConnectionContext.ConnectAsUser = $true + $sqlConnectionContext.ConnectAsUserName = $connectUserName + $sqlConnectionContext.ConnectAsUserPassword = $Credential.GetNetworkCredential().Password + } + } + + try + { + $onlineStatus = 'Online' + $connectTimer = [System.Diagnostics.StopWatch]::StartNew() + $sqlConnectionContext.Connect() + + <# + The addition of the ConnectTimeout property to the ConnectionContext will force the + Connect() method to block until successful. THe SMO object's Status property may not + report 'Online' immediately even though the Connect() was successful. The loop is to + ensure the SMO's Status property was been updated. + #> + $sleepInSeconds = 2 + do + { + $instanceStatus = $sqlServerObject.Status + if ([System.String]::IsNullOrEmpty($instanceStatus)) + { + $instanceStatus = 'Unknown' + } + else + { + # Property Status is of type Enum ServerStatus, we return the string equivalent. + $instanceStatus = $instanceStatus.ToString() + } + + if ($instanceStatus -eq $onlineStatus) + { + break + } + + Write-Debug -Message ( + $script:localizedData.WaitForDatabaseEngineInstanceStatus -f $instanceStatus, $onlineStatus, $sleepInSeconds + ) + + Start-Sleep -Seconds $sleepInSeconds + $sqlServerObject.Refresh() + } while ($connectTimer.Elapsed.TotalSeconds -lt $StatementTimeout) + + if ($instanceStatus -match '^Online$') + { + Write-Verbose -Message ( + $script:localizedData.ConnectedToDatabaseEngineInstance -f $databaseEngineInstance + ) + + return $sqlServerObject + } + else + { + $errorMessage = $script:localizedData.DatabaseEngineInstanceNotOnline -f @( + $databaseEngineInstance, + $instanceStatus + ) + + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' -ArgumentList @($errorMessage) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException, + 'CS0001', + 'InvalidOperation', + $databaseEngineInstance + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + Write-Error -ErrorRecord $errorRecordToThrow + } + } + catch + { + $errorMessage = $script:localizedData.FailedToConnectToDatabaseEngineInstance -f $databaseEngineInstance + + $invalidOperationException = New-Object -TypeName 'InvalidOperationException' -ArgumentList @($errorMessage, $_.Exception) + + $newObjectParameters = @{ + TypeName = 'System.Management.Automation.ErrorRecord' + ArgumentList = @( + $invalidOperationException, + 'CS0002', + 'InvalidOperation', + $databaseEngineInstance + ) + } + + $errorRecordToThrow = New-Object @newObjectParameters + + Write-Error -ErrorRecord $errorRecordToThrow + } + finally + { + $connectTimer.Stop() + <# + Connect will ensure we actually can connect, but we need to disconnect + from the session so we don't have anything hanging. If we need run a + method on the returned $sqlServerObject it will automatically open a + new session and then close, therefore we don't need to keep this + session open. + #> + $sqlConnectionContext.Disconnect() + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Connect-SqlAnalysis.ps1 b/source/Modules/SqlServerDsc.Common/Public/Connect-SqlAnalysis.ps1 new file mode 100644 index 000000000..2fc2012c2 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Connect-SqlAnalysis.ps1 @@ -0,0 +1,115 @@ +<# + .SYNOPSIS + Connect to a SQL Server Analysis Service and return the server object. + + .PARAMETER ServerName + String containing the host name of the SQL Server to connect to. + + .PARAMETER InstanceName + String containing the SQL Server Analysis Service instance to connect to. + + .PARAMETER SetupCredential + PSCredential object with the credentials to use to impersonate a user when + connecting. If this is not provided then the current user will be used to + connect to the SQL Server Analysis Service instance. +#> +function Connect-SqlAnalysis +{ + [CmdletBinding()] + param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $ServerName = (Get-ComputerName), + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $InstanceName = 'MSSQLSERVER', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $SetupCredential, + + [Parameter()] + [System.String[]] + $FeatureFlag + ) + + if ($InstanceName -eq 'MSSQLSERVER') + { + $analysisServiceInstance = $ServerName + } + else + { + $analysisServiceInstance = "$ServerName\$InstanceName" + } + + if ($SetupCredential) + { + $userName = $SetupCredential.UserName + $password = $SetupCredential.GetNetworkCredential().Password + + $analysisServicesDataSource = "Data Source=$analysisServiceInstance;User ID=$userName;Password=$password" + } + else + { + $analysisServicesDataSource = "Data Source=$analysisServiceInstance" + } + + try + { + if ((Test-FeatureFlag -FeatureFlag $FeatureFlag -TestFlag 'AnalysisServicesConnection')) + { + Import-SqlDscPreferredModule + + $analysisServicesObject = New-Object -TypeName 'Microsoft.AnalysisServices.Server' + + if ($analysisServicesObject) + { + $analysisServicesObject.Connect($analysisServicesDataSource) + } + + if ((-not $analysisServicesObject) -or ($analysisServicesObject -and $analysisServicesObject.Connected -eq $false)) + { + $errorMessage = $script:localizedData.FailedToConnectToAnalysisServicesInstance -f $analysisServiceInstance + + New-InvalidOperationException -Message $errorMessage + } + else + { + Write-Verbose -Message ($script:localizedData.ConnectedToAnalysisServicesInstance -f $analysisServiceInstance) -Verbose + } + } + else + { + $null = Import-Assembly -Name 'Microsoft.AnalysisServices' -LoadWithPartialName + + $analysisServicesObject = New-Object -TypeName 'Microsoft.AnalysisServices.Server' + + if ($analysisServicesObject) + { + $analysisServicesObject.Connect($analysisServicesDataSource) + } + else + { + $errorMessage = $script:localizedData.FailedToConnectToAnalysisServicesInstance -f $analysisServiceInstance + + New-InvalidOperationException -Message $errorMessage + } + + Write-Verbose -Message ($script:localizedData.ConnectedToAnalysisServicesInstance -f $analysisServiceInstance) -Verbose + } + } + catch + { + $errorMessage = $script:localizedData.FailedToConnectToAnalysisServicesInstance -f $analysisServiceInstance + + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + return $analysisServicesObject +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Connect-UncPath.ps1 b/source/Modules/SqlServerDsc.Common/Public/Connect-UncPath.ps1 new file mode 100644 index 000000000..5b2ec8f10 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Connect-UncPath.ps1 @@ -0,0 +1,56 @@ +<# + .SYNOPSIS + Connects to the UNC path provided in the parameter SourcePath. + Optionally connects using the provided credentials. + + .PARAMETER SourcePath + Source path to connect to. + + .PARAMETER SourceCredential + The credentials to access the path provided in SourcePath. + + .PARAMETER PassThru + If used, returns a MSFT_SmbMapping object that represents the newly + created SMB mapping. + + .OUTPUTS + Returns a MSFT_SmbMapping object that represents the newly created + SMB mapping (ony when used with parameter PassThru). +#> +function Connect-UncPath +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RemotePath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $SourceCredential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru + ) + + $newSmbMappingParameters = @{ + RemotePath = $RemotePath + } + + if ($PSBoundParameters.ContainsKey('SourceCredential')) + { + $newSmbMappingParameters['UserName'] = $SourceCredential.UserName + $newSmbMappingParameters['Password'] = $SourceCredential.GetNetworkCredential().Password + } + + $newSmbMappingResult = New-SmbMapping @newSmbMappingParameters + + if ($PassThru.IsPresent) + { + return $newSmbMappingResult + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/ConvertTo-ServerInstanceName.ps1 b/source/Modules/SqlServerDsc.Common/Public/ConvertTo-ServerInstanceName.ps1 new file mode 100644 index 000000000..d02165d67 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/ConvertTo-ServerInstanceName.ps1 @@ -0,0 +1,37 @@ +<# + .SYNOPSIS + Converts the combination of server name and instance name to + the correct server instance name. + + .PARAMETER InstanceName + Specifies the name of the SQL Server instance on the host. + + .PARAMETER ServerName + Specifies the host name of the SQL Server. +#> +function ConvertTo-ServerInstanceName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + $ServerName + ) + + if ($InstanceName -eq 'MSSQLSERVER') + { + $serverInstance = $ServerName + } + else + { + $serverInstance = '{0}\{1}' -f $ServerName, $InstanceName + } + + return $serverInstance +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Copy-ItemWithRobocopy.ps1 b/source/Modules/SqlServerDsc.Common/Public/Copy-ItemWithRobocopy.ps1 new file mode 100644 index 000000000..78ad58d60 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Copy-ItemWithRobocopy.ps1 @@ -0,0 +1,99 @@ +<# + .SYNOPSIS + Copy folder structure using Robocopy. Every file and folder, including empty ones are copied. + + .PARAMETER Path + Source path to be copied. + + .PARAMETER DestinationPath + The path to the destination. +#> +function Copy-ItemWithRobocopy +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Path, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DestinationPath + ) + + $quotedPath = '"{0}"' -f $Path + $quotedDestinationPath = '"{0}"' -f $DestinationPath + $robocopyExecutable = Get-Command -Name 'Robocopy.exe' -ErrorAction 'Stop' + + $robocopyArgumentSilent = '/njh /njs /ndl /nc /ns /nfl' + $robocopyArgumentCopySubDirectoriesIncludingEmpty = '/e' + $robocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource = '/purge' + + if ([System.Version]$robocopyExecutable.FileVersionInfo.ProductVersion -ge [System.Version]'6.3.9600.16384') + { + Write-Verbose -Message $script:localizedData.RobocopyUsingUnbufferedIo -Verbose + + $robocopyArgumentUseUnbufferedIO = '/J' + } + else + { + Write-Verbose -Message $script:localizedData.RobocopyNotUsingUnbufferedIo -Verbose + } + + $robocopyArgumentList = '{0} {1} {2} {3} {4} {5}' -f @( + $quotedPath, + $quotedDestinationPath, + $robocopyArgumentCopySubDirectoriesIncludingEmpty, + $robocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource, + $robocopyArgumentUseUnbufferedIO, + $robocopyArgumentSilent + ) + + $robocopyStartProcessParameters = @{ + FilePath = $robocopyExecutable.Name + ArgumentList = $robocopyArgumentList + } + + Write-Verbose -Message ($script:localizedData.RobocopyArguments -f $robocopyArgumentList) -Verbose + $robocopyProcess = Start-Process @robocopyStartProcessParameters -Wait -NoNewWindow -PassThru + + switch ($($robocopyProcess.ExitCode)) + { + { $_ -in 8, 16 } + { + $errorMessage = $script:localizedData.RobocopyErrorCopying -f $_ + New-InvalidOperationException -Message $errorMessage + } + + { $_ -gt 7 } + { + $errorMessage = $script:localizedData.RobocopyFailuresCopying -f $_ + New-InvalidResultException -Message $errorMessage + } + + 1 + { + Write-Verbose -Message $script:localizedData.RobocopySuccessful -Verbose + } + + 2 + { + Write-Verbose -Message $script:localizedData.RobocopyRemovedExtraFilesAtDestination -Verbose + } + + 3 + { + Write-Verbose -Message ( + '{0} {1}' -f $script:localizedData.RobocopySuccessful, $script:localizedData.RobocopyRemovedExtraFilesAtDestination + ) -Verbose + } + + { $_ -eq 0 -or $null -eq $_ } + { + Write-Verbose -Message $script:localizedData.RobocopyAllFilesPresent -Verbose + } + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Disconnect-UncPath.ps1 b/source/Modules/SqlServerDsc.Common/Public/Disconnect-UncPath.ps1 new file mode 100644 index 000000000..e649eb6cf --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Disconnect-UncPath.ps1 @@ -0,0 +1,20 @@ +<# + .SYNOPSIS + Disconnects from the UNC path provided in the parameter SourcePath. + + .PARAMETER SourcePath + Source path to disconnect from. +#> +function Disconnect-UncPath +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $RemotePath + ) + + Remove-SmbMapping -RemotePath $RemotePath -Force +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Find-ExceptionByNumber.ps1 b/source/Modules/SqlServerDsc.Common/Public/Find-ExceptionByNumber.ps1 new file mode 100644 index 000000000..cf99cf043 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Find-ExceptionByNumber.ps1 @@ -0,0 +1,47 @@ +<# + .SYNOPSIS + Recursively searches Exception stack for specific error number. + + .PARAMETER ExceptionToSearch + The Exception object to test + + .PARAMETER ErrorNumber + The specific error number to look for + + .NOTES + This function allows us to more easily write mocks. +#> +function Find-ExceptionByNumber +{ + # Define parameters + param + ( + [Parameter(Mandatory = $true)] + [System.Exception] + $ExceptionToSearch, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorNumber + ) + + # Define working variables + $errorFound = $false + + # Check to see if the exception has an inner exception + if ($ExceptionToSearch.InnerException) + { + # Assign found to the returned recursive call + $errorFound = Find-ExceptionByNumber -ExceptionToSearch $ExceptionToSearch.InnerException -ErrorNumber $ErrorNumber + } + + # Check to see if it was found + if (!$errorFound) + { + # Check this exceptions message + $errorFound = $ExceptionToSearch.Number -eq $ErrorNumber + } + + # Return + return $errorFound +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Get-PrimaryReplicaServerObject.ps1 b/source/Modules/SqlServerDsc.Common/Public/Get-PrimaryReplicaServerObject.ps1 new file mode 100644 index 000000000..3dc133d91 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Get-PrimaryReplicaServerObject.ps1 @@ -0,0 +1,33 @@ +<# + .SYNOPSIS + Get the server object of the primary replica of the specified availability group. + + .PARAMETER ServerObject + The current server object connection. + + .PARAMETER AvailabilityGroup + The availability group object used to find the primary replica server name. +#> +function Get-PrimaryReplicaServerObject +{ + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.AvailabilityGroup] + $AvailabilityGroup + ) + + $primaryReplicaServerObject = $serverObject + + # Determine if we're connected to the primary replica + if ( ( $AvailabilityGroup.PrimaryReplicaServerName -ne $serverObject.DomainInstanceName ) -and ( -not [System.String]::IsNullOrEmpty($AvailabilityGroup.PrimaryReplicaServerName) ) ) + { + $primaryReplicaServerObject = Connect-SQL -ServerName $AvailabilityGroup.PrimaryReplicaServerName -ErrorAction 'Stop' + } + + return $primaryReplicaServerObject +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Get-ServiceAccount.ps1 b/source/Modules/SqlServerDsc.Common/Public/Get-ServiceAccount.ps1 new file mode 100644 index 000000000..08b9658f5 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Get-ServiceAccount.ps1 @@ -0,0 +1,56 @@ +<# + .SYNOPSIS + Builds service account parameters for service account. + + .PARAMETER ServiceAccount + Credential for the service account. +#> +function Get-ServiceAccount +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $ServiceAccount + ) + + $accountParameters = @{ } + + switch -Regex ($ServiceAccount.UserName.ToUpper()) + { + '^(?:NT ?AUTHORITY\\)?(SYSTEM|LOCALSERVICE|LOCAL SERVICE|NETWORKSERVICE|NETWORK SERVICE)$' + { + $accountParameters = @{ + UserName = "NT AUTHORITY\$($Matches[1])" + } + } + + '^(?:NT SERVICE\\)(.*)$' + { + $accountParameters = @{ + UserName = "NT SERVICE\$($Matches[1])" + } + } + + # Testing if account is a Managed Service Account, which ends with '$'. + '\$$' + { + $accountParameters = @{ + UserName = $ServiceAccount.UserName + } + } + + # Normal local or domain service account. + default + { + $accountParameters = @{ + UserName = $ServiceAccount.UserName + Password = $ServiceAccount.GetNetworkCredential().Password + } + } + } + + return $accountParameters +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Get-SqlInstanceMajorVersion.ps1 b/source/Modules/SqlServerDsc.Common/Public/Get-SqlInstanceMajorVersion.ps1 new file mode 100644 index 000000000..0326efc5e --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Get-SqlInstanceMajorVersion.ps1 @@ -0,0 +1,35 @@ +<# + .SYNOPSIS + Returns the major SQL version for the specific instance. + + .PARAMETER InstanceName + String containing the name of the SQL instance to be configured. Default + value is 'MSSQLSERVER'. + + .OUTPUTS + System.UInt16. Returns the SQL Server major version number. +#> +function Get-SqlInstanceMajorVersion +{ + [CmdletBinding()] + [OutputType([System.UInt16])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName + ) + + $sqlInstanceId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').$InstanceName + $sqlVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$sqlInstanceId\Setup").Version + + if (-not $sqlVersion) + { + $errorMessage = $script:localizedData.SqlServerVersionIsInvalid -f $InstanceName + New-InvalidResultException -Message $errorMessage + } + + [System.UInt16] $sqlMajorVersionNumber = $sqlVersion.Split('.')[0] + + return $sqlMajorVersionNumber +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Import-Assembly.ps1 b/source/Modules/SqlServerDsc.Common/Public/Import-Assembly.ps1 new file mode 100644 index 000000000..3ebbc5293 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Import-Assembly.ps1 @@ -0,0 +1,75 @@ +<# + .SYNOPSIS + Imports the assembly into the session. + + .DESCRIPTION + Imports the assembly into the session and returns a reference to the + assembly. + + .PARAMETER Name + Specifies the name of the assembly to load. + + .PARAMETER LoadWithPartialName + Specifies if the imported assembly should be the first found in GAC, + regardless of version. + + .OUTPUTS + [System.Reflection.Assembly] + + Returns a reference to the assembly object. + + .EXAMPLE + Import-Assembly -Name "Microsoft.SqlServer.ConnectionInfo, Version=$SqlMajorVersion.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" + + .EXAMPLE + Import-Assembly -Name 'Microsoft.AnalysisServices' -LoadWithPartialName + + .NOTES + This should normally work using Import-Module and New-Object instead of + using the method [System.Reflection.Assembly]::Load(). But due to a + missing assembly in the module SqlServer this is still needed. + + Import-Module SqlServer + $connectionInfo = New-Object -TypeName 'Microsoft.SqlServer.Management.Common.ServerConnection' -ArgumentList @('testclu01a\SQL2014') + # Missing assembly 'Microsoft.SqlServer.Rmo' in module SqlServer prevents this call from working. + $replication = New-Object -TypeName 'Microsoft.SqlServer.Replication.ReplicationServer' -ArgumentList @($connectionInfo) +#> +function Import-Assembly +{ + [CmdletBinding()] + [OutputType([System.Reflection.Assembly])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $LoadWithPartialName + ) + + try + { + if ($LoadWithPartialName.IsPresent) + { + $assemblyInformation = [System.Reflection.Assembly]::LoadWithPartialName($Name) + } + else + { + $assemblyInformation = [System.Reflection.Assembly]::Load($Name) + } + + Write-Verbose -Message ( + $script:localizedData.LoadedAssembly -f $assemblyInformation.FullName + ) + } + catch + { + $errorMessage = $script:localizedData.FailedToLoadAssembly -f $Name + + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + + return $assemblyInformation +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Invoke-InstallationMediaCopy.ps1 b/source/Modules/SqlServerDsc.Common/Public/Invoke-InstallationMediaCopy.ps1 new file mode 100644 index 000000000..51c206013 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Invoke-InstallationMediaCopy.ps1 @@ -0,0 +1,66 @@ +<# + .SYNOPSIS + Connects to the source using the provided credentials and then uses + robocopy to download the installation media to a local temporary folder. + + .PARAMETER SourcePath + Source path to be copied. + + .PARAMETER SourceCredential + The credentials to access the SourcePath. + + .PARAMETER PassThru + If used, returns the destination path as string. + + .OUTPUTS + Returns the destination path (when used with the parameter PassThru). +#> +function Invoke-InstallationMediaCopy +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $SourcePath, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $SourceCredential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru + ) + + Connect-UncPath -RemotePath $SourcePath -SourceCredential $SourceCredential + + $SourcePath = $SourcePath.TrimEnd('/\') + <# + Create a destination folder so the media files aren't written + to the root of the Temp folder. + #> + $serverName, $shareName, $leafs = ($SourcePath -replace '\\\\') -split '\\' + if ($leafs) + { + $mediaDestinationFolder = $leafs | Select-Object -Last 1 + } + else + { + $mediaDestinationFolder = New-Guid | Select-Object -ExpandProperty Guid + } + + $mediaDestinationPath = Join-Path -Path (Get-TemporaryFolder) -ChildPath $mediaDestinationFolder + + Write-Verbose -Message ($script:localizedData.RobocopyIsCopying -f $SourcePath, $mediaDestinationPath) -Verbose + Copy-ItemWithRobocopy -Path $SourcePath -DestinationPath $mediaDestinationPath + + Disconnect-UncPath -RemotePath $SourcePath + + if ($PassThru.IsPresent) + { + return $mediaDestinationPath + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Invoke-SqlScript.ps1 b/source/Modules/SqlServerDsc.Common/Public/Invoke-SqlScript.ps1 new file mode 100644 index 000000000..6bd658bec --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Invoke-SqlScript.ps1 @@ -0,0 +1,145 @@ +<# + .SYNOPSIS + Execute an SQL script located in a file on disk. + + .PARAMETER ServerInstance + The name of an instance of the Database Engine. + For default instances, only specify the computer name. For named instances, + use the format ComputerName\InstanceName. + + .PARAMETER InputFile + Path to SQL script file that will be executed. + + .PARAMETER Query + The full query that will be executed. + + .PARAMETER Credential + The credentials to use to authenticate using SQL Authentication. To + authenticate using Windows Authentication, assign the credentials + to the built-in parameter 'PsDscRunAsCredential'. If both parameters + 'Credential' and 'PsDscRunAsCredential' are not assigned, then the + SYSTEM account will be used to authenticate using Windows Authentication. + + .PARAMETER QueryTimeout + Specifies, as an integer, the number of seconds after which the T-SQL + script execution will time out. In some SQL Server versions there is a + bug in Invoke-SqlCmd where the normal default value 0 (no timeout) is not + respected and the default value is incorrectly set to 30 seconds. + + .PARAMETER Variable + Creates a Invoke-SqlCmd scripting variable for use in the Invoke-SqlCmd + script, and sets a value for the variable. + + .PARAMETER DisableVariables + Specifies, as a boolean, whether or not PowerShell will ignore Invoke-SqlCmd + scripting variables that share a format such as $(variable_name). For more + information how to use this, please go to the help documentation for + [Invoke-SqlCmd](https://docs.microsoft.com/en-us/powershell/module/sqlserver/Invoke-Sqlcmd). + + .PARAMETER Encrypt + Specifies how encryption should be enforced. When not specified, the default + value is `Mandatory`. + + This value maps to the Encrypt property SqlConnectionEncryptOption + on the SqlConnection object of the Microsoft.Data.SqlClient driver. + + This parameter can only be used when the module SqlServer v22.x.x is installed. + + .NOTES + This wrapper for Invoke-SqlCmd make verbose functionality of PRINT and + RAISEERROR statements work as those are outputted in the verbose output + stream. For some reason having the wrapper in a separate module seems to + trigger (so that it works getting) the verbose output for those statements. + + Parameter `Encrypt` controls whether the connection used by `Invoke-SqlCmd` + should enforce encryption. This parameter can only be used together with the + module _SqlServer_ v22.x (minimum v22.0.49-preview). The parameter will be + ignored if an older major versions of the module _SqlServer_ is used. + Encryption is mandatory by default, which generates the following exception + when the correct certificates are not present: + + "A connection was successfully established with the server, but then + an error occurred during the login process. (provider: SSL Provider, + error: 0 - The certificate chain was issued by an authority that is + not trusted.)" + + For more details, see the article [Connect to SQL Server with strict encryption](https://learn.microsoft.com/en-us/sql/relational-databases/security/networking/connect-with-strict-encryption?view=sql-server-ver16) + and [Configure SQL Server Database Engine for encrypting connections](https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-sql-server-encryption?view=sql-server-ver16). +#> +function Invoke-SqlScript +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ServerInstance, + + [Parameter(ParameterSetName = 'File', Mandatory = $true)] + [System.String] + $InputFile, + + [Parameter(ParameterSetName = 'Query', Mandatory = $true)] + [System.String] + $Query, + + [Parameter()] + [System.Management.Automation.PSCredential] + [System.Management.Automation.Credential()] + $Credential, + + [Parameter()] + [System.UInt32] + $QueryTimeout, + + [Parameter()] + [System.String[]] + $Variable, + + [Parameter()] + [System.Boolean] + $DisableVariables, + + [Parameter()] + [ValidateSet('Mandatory', 'Optional', 'Strict')] + [System.String] + $Encrypt + ) + + Import-SqlDscPreferredModule + + if ($PSCmdlet.ParameterSetName -eq 'File') + { + $null = $PSBoundParameters.Remove('Query') + } + elseif ($PSCmdlet.ParameterSetName -eq 'Query') + { + $null = $PSBoundParameters.Remove('InputFile') + } + + if ($null -ne $Credential) + { + $null = $PSBoundParameters.Add('Username', $Credential.UserName) + + $null = $PSBoundParameters.Add('Password', $Credential.GetNetworkCredential().Password) + } + + $null = $PSBoundParameters.Remove('Credential') + + if ($PSBoundParameters.ContainsKey('Encrypt')) + { + $commandInvokeSqlCmd = Get-Command -Name 'Invoke-SqlCmd' + + if ($null -ne $commandInvokeSqlCmd -and $commandInvokeSqlCmd.Parameters.Keys -notcontains 'Encrypt') + { + $null = $PSBoundParameters.Remove('Encrypt') + } + } + + if ([System.String]::IsNullOrEmpty($Variable)) + { + $null = $PSBoundParameters.Remove('Variable') + } + + Invoke-SqlCmd @PSBoundParameters +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Restart-SqlClusterService.ps1 b/source/Modules/SqlServerDsc.Common/Public/Restart-SqlClusterService.ps1 new file mode 100644 index 000000000..ff9a23930 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Restart-SqlClusterService.ps1 @@ -0,0 +1,124 @@ +<# + .SYNOPSIS + Restarts a SQL Server cluster instance and associated services + + .PARAMETER InstanceName + Specifies the instance name that matches a SQL Server MSCluster_Resource + property .PrivateProperties.InstanceName. + + .PARAMETER Timeout + Timeout value for restarting the SQL services. The default value is 120 seconds. + + .PARAMETER OwnerNode + Specifies a list of owner nodes names of a cluster groups. If the SQL Server + instance is a Failover Cluster instance then the cluster group will only + be taken offline and back online when the owner of the cluster group is + one of the nodes specified in this list. These node names specified in this + parameter must match the Owner property of the cluster resource, for example + @('sqltest10', 'SQLTEST11'). The names are case-insensitive. + If this parameter is not specified the cluster group will be taken offline + and back online regardless of owner. +#> +function Restart-SqlClusterService +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter()] + [System.UInt32] + $Timeout = 120, + + [Parameter()] + [System.String[]] + $OwnerNode + ) + + # Get the cluster resources + Write-Verbose -Message ($script:localizedData.GetSqlServerClusterResources) -Verbose + + $sqlService = Get-CimInstance -Namespace 'root/MSCluster' -ClassName 'MSCluster_Resource' -Filter "Type = 'SQL Server'" | + Where-Object -FilterScript { + $_.PrivateProperties.InstanceName -eq $InstanceName -and $_.State -eq 2 + } + + # If the cluster resource is found and online then continue. + if ($sqlService) + { + $isOwnerOfClusterResource = $true + + if ($PSBoundParameters.ContainsKey('OwnerNode') -and $sqlService.OwnerNode -notin $OwnerNode) + { + $isOwnerOfClusterResource = $false + } + + if ($isOwnerOfClusterResource) + { + Write-Verbose -Message ($script:localizedData.GetSqlAgentClusterResource) -Verbose + + $agentService = $sqlService | + Get-CimAssociatedInstance -ResultClassName MSCluster_Resource | + Where-Object -FilterScript { + $_.Type -eq 'SQL Server Agent' -and $_.State -eq 2 + } + + # Build a listing of resources being acted upon + $resourceNames = @($sqlService.Name, ($agentService | + Select-Object -ExpandProperty Name)) -join "', '" + + # Stop the SQL Server and dependent resources + Write-Verbose -Message ($script:localizedData.BringClusterResourcesOffline -f $resourceNames) -Verbose + + $sqlService | + Invoke-CimMethod -MethodName TakeOffline -Arguments @{ + Timeout = $Timeout + } + + # Start the SQL server resource + Write-Verbose -Message ($script:localizedData.BringSqlServerClusterResourcesOnline) -Verbose + + $sqlService | + Invoke-CimMethod -MethodName BringOnline -Arguments @{ + Timeout = $Timeout + } + + # Start the SQL Agent resource + if ($agentService) + { + if ($PSBoundParameters.ContainsKey('OwnerNode') -and $agentService.OwnerNode -notin $OwnerNode) + { + $isOwnerOfClusterResource = $false + } + + if ($isOwnerOfClusterResource) + { + Write-Verbose -Message ($script:localizedData.BringSqlServerAgentClusterResourcesOnline) -Verbose + + $agentService | + Invoke-CimMethod -MethodName BringOnline -Arguments @{ + Timeout = $Timeout + } + } + else + { + Write-Verbose -Message ( + $script:localizedData.NotOwnerOfClusterResource -f (Get-ComputerName), $agentService.Name, $agentService.OwnerNode + ) -Verbose + } + } + } + else + { + Write-Verbose -Message ( + $script:localizedData.NotOwnerOfClusterResource -f (Get-ComputerName), $sqlService.Name, $sqlService.OwnerNode + ) -Verbose + } + } + else + { + Write-Warning -Message ($script:localizedData.ClusterResourceNotFoundOrOffline -f $InstanceName) + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Restart-SqlService.ps1 b/source/Modules/SqlServerDsc.Common/Public/Restart-SqlService.ps1 new file mode 100644 index 000000000..78afd006b --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Restart-SqlService.ps1 @@ -0,0 +1,198 @@ +<# + .SYNOPSIS + Restarts a SQL Server instance and associated services + + .PARAMETER ServerName + Hostname of the SQL Server to be configured + + .PARAMETER InstanceName + Name of the SQL instance to be configured. Default is 'MSSQLSERVER' + + .PARAMETER Timeout + Timeout value for restarting the SQL services. The default value is 120 seconds. + + .PARAMETER SkipClusterCheck + If cluster check should be skipped. If this is present no connection + is made to the instance to check if the instance is on a cluster. + + This need to be used for some resource, for example for the SqlServerNetwork + resource when it's used to enable a disable protocol. + + .PARAMETER SkipWaitForOnline + If this is present no connection is made to the instance to check if the + instance is online. + + This need to be used for some resource, for example for the SqlServerNetwork + resource when it's used to disable protocol. + + .PARAMETER OwnerNode + Specifies a list of owner nodes names of a cluster groups. If the SQL Server + instance is a Failover Cluster instance then the cluster group will only + be taken offline and back online when the owner of the cluster group is + one of the nodes specified in this list. These node names specified in this + parameter must match the Owner property of the cluster resource, for example + @('sqltest10', 'SQLTEST11'). The names are case-insensitive. + If this parameter is not specified the cluster group will be taken offline + and back online regardless of owner. + + .EXAMPLE + Restart-SqlService -ServerName localhost + + .EXAMPLE + Restart-SqlService -ServerName localhost -InstanceName 'NamedInstance' + + .EXAMPLE + Restart-SqlService -ServerName localhost -InstanceName 'NamedInstance' -SkipClusterCheck -SkipWaitForOnline + + .EXAMPLE + Restart-SqlService -ServerName CLU01 -Timeout 300 + + .EXAMPLE + Restart-SqlService -ServerName CLU01 -Timeout 300 -OwnerNode 'testclu10' +#> +function Restart-SqlService +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $ServerName, + + [Parameter()] + [System.String] + $InstanceName = 'MSSQLSERVER', + + [Parameter()] + [System.UInt32] + $Timeout = 120, + + [Parameter()] + [Switch] + $SkipClusterCheck, + + [Parameter()] + [Switch] + $SkipWaitForOnline, + + [Parameter()] + [System.String[]] + $OwnerNode + ) + + $restartWindowsService = $true + + # Check if a cluster, otherwise assume that a Windows service should be restarted. + if (-not $SkipClusterCheck.IsPresent) + { + ## Connect to the instance + $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' + + if ($serverObject.IsClustered) + { + # Make sure Windows service is not restarted outside of the cluster. + $restartWindowsService = $false + + $restartSqlClusterServiceParameters = @{ + InstanceName = $serverObject.ServiceName + } + + if ($PSBoundParameters.ContainsKey('Timeout')) + { + $restartSqlClusterServiceParameters['Timeout'] = $Timeout + } + + if ($PSBoundParameters.ContainsKey('OwnerNode')) + { + $restartSqlClusterServiceParameters['OwnerNode'] = $OwnerNode + } + + Restart-SqlClusterService @restartSqlClusterServiceParameters + } + } + + if ($restartWindowsService) + { + if ($InstanceName -eq 'MSSQLSERVER') + { + $serviceName = 'MSSQLSERVER' + } + else + { + $serviceName = 'MSSQL${0}' -f $InstanceName + } + + Write-Verbose -Message ($script:localizedData.GetServiceInformation -f 'SQL Server') -Verbose + + $sqlService = Get-Service -Name $serviceName + + <# + Get all dependent services that are running. + There are scenarios where an automatic service is stopped and should not be restarted automatically. + #> + $agentService = $sqlService.DependentServices | + Where-Object -FilterScript { $_.Status -eq 'Running' } + + # Restart the SQL Server service + Write-Verbose -Message ($script:localizedData.RestartService -f 'SQL Server') -Verbose + $sqlService | + Restart-Service -Force + + # Start dependent services + $agentService | + ForEach-Object -Process { + Write-Verbose -Message ($script:localizedData.StartingDependentService -f $_.DisplayName) -Verbose + $_ | Start-Service + } + } + + Write-Verbose -Message ($script:localizedData.WaitingInstanceTimeout -f $ServerName, $InstanceName, $Timeout) -Verbose + + if (-not $SkipWaitForOnline.IsPresent) + { + $connectTimer = [System.Diagnostics.StopWatch]::StartNew() + + $connectSqlError = $null + + do + { + # This call, if it fails, will take between ~9-10 seconds to return. + $testConnectionServerObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'SilentlyContinue' -ErrorVariable 'connectSqlError' + + # Make sure we have an SMO object to test Status + if ($testConnectionServerObject) + { + if ($testConnectionServerObject.Status -eq 'Online') + { + break + } + } + + # Waiting 2 seconds to not hammer the SQL Server instance. + Start-Sleep -Seconds 2 + } until ($connectTimer.Elapsed.TotalSeconds -ge $Timeout) + + $connectTimer.Stop() + + # Was the timeout period reach before able to connect to the SQL Server instance? + if (-not $testConnectionServerObject -or $testConnectionServerObject.Status -ne 'Online') + { + $errorMessage = $script:localizedData.FailedToConnectToInstanceTimeout -f @( + $ServerName, + $InstanceName, + $Timeout + ) + + $newInvalidOperationExceptionParameters = @{ + Message = $errorMessage + } + + if ($connectSqlError) + { + $newInvalidOperationExceptionParameters.ErrorRecord = $connectSqlError[$connectSqlError.Count - 1] + } + + New-InvalidOperationException @newInvalidOperationExceptionParameters + } + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Split-FullSqlInstanceName.ps1 b/source/Modules/SqlServerDsc.Common/Public/Split-FullSqlInstanceName.ps1 new file mode 100644 index 000000000..ceb662aa7 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Split-FullSqlInstanceName.ps1 @@ -0,0 +1,33 @@ +<# + .SYNOPSIS + Takes a SQL Instance name in the format of 'Server\Instance' and splits + it into a hash table prepared to be passed into Connect-SQL. + + .PARAMETER FullSqlInstanceName + The full SQL instance name string to be split. + + .OUTPUTS + Hash table with the properties ServerName and InstanceName. +#> +function Split-FullSqlInstanceName +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $FullSqlInstanceName + ) + + $sqlServer, $sqlInstanceName = $FullSqlInstanceName.Split('\') + + if ( [System.String]::IsNullOrEmpty($sqlInstanceName) ) + { + $sqlInstanceName = 'MSSQLSERVER' + } + + return @{ + ServerName = $sqlServer + InstanceName = $sqlInstanceName + } +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Start-SqlSetupProcess.ps1 b/source/Modules/SqlServerDsc.Common/Public/Start-SqlSetupProcess.ps1 new file mode 100644 index 000000000..39758870d --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Start-SqlSetupProcess.ps1 @@ -0,0 +1,44 @@ +<# + .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' + + Write-Verbose -Message ($script:localizedData.StartSetupProcess -f $sqlSetupProcess.Id, $startProcessParameters.FilePath, $Timeout) -Verbose + + Wait-Process -InputObject $sqlSetupProcess -Timeout $Timeout -ErrorAction 'Stop' + + return $sqlSetupProcess.ExitCode +} + diff --git a/source/Modules/SqlServerDsc.Common/Public/Test-ActiveNode.ps1 b/source/Modules/SqlServerDsc.Common/Public/Test-ActiveNode.ps1 new file mode 100644 index 000000000..a8ca82b7b --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Test-ActiveNode.ps1 @@ -0,0 +1,40 @@ +<# + .SYNOPSIS + Determine if the current node is hosting the instance. + + .PARAMETER ServerObject + The server object on which to perform the test. +#> +function Test-ActiveNode +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject + ) + + $result = $false + + # Determine if this is a failover cluster instance (FCI) + if ( $ServerObject.IsMemberOfWsfcCluster ) + { + <# + If the current node name is the same as the name the instances is + running on, then this is the active node + #> + $result = $ServerObject.ComputerNamePhysicalNetBIOS -eq (Get-ComputerName) + } + else + { + <# + This is a standalone instance, therefore the node will always host + the instance. + #> + $result = $true + } + + return $result +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Test-AvailabilityReplicaSeedingModeAutomatic.ps1 b/source/Modules/SqlServerDsc.Common/Public/Test-AvailabilityReplicaSeedingModeAutomatic.ps1 new file mode 100644 index 000000000..36ef56d13 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Test-AvailabilityReplicaSeedingModeAutomatic.ps1 @@ -0,0 +1,73 @@ +<# + .SYNOPSIS + Determine if the seeding mode of the specified availability group is automatic. + + .PARAMETER ServerName + The hostname of the server that hosts the SQL instance. + + .PARAMETER InstanceName + The name of the SQL instance that hosts the availability group. + + .PARAMETER AvailabilityGroupName + The name of the availability group to check. + + .PARAMETER AvailabilityReplicaName + The name of the availability replica to check. +#> +function Test-AvailabilityReplicaSeedingModeAutomatic +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServerName, + + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $AvailabilityGroupName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $AvailabilityReplicaName + ) + + # Assume automatic seeding is disabled by default + $availabilityReplicaSeedingModeAutomatic = $false + + $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' + + # Only check the seeding mode if this is SQL 2016 or newer + if ( $serverObject.Version -ge 13 ) + { + $invokeSqlDscQueryParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + DatabaseName = 'master' + PassThru = $true + } + + $queryToGetSeedingMode = " + SELECT seeding_mode_desc + FROM sys.availability_replicas ar + INNER JOIN sys.availability_groups ag ON ar.group_id = ag.group_id + WHERE ag.name = '$AvailabilityGroupName' + AND ar.replica_server_name = '$AvailabilityReplicaName' + " + $seedingModeResults = Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $queryToGetSeedingMode + $seedingMode = $seedingModeResults.Tables.Rows.seeding_mode_desc + + if ( $seedingMode -eq 'Automatic' ) + { + $availabilityReplicaSeedingModeAutomatic = $true + } + } + + return $availabilityReplicaSeedingModeAutomatic +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Test-ClusterPermissions.ps1 b/source/Modules/SqlServerDsc.Common/Public/Test-ClusterPermissions.ps1 new file mode 100644 index 000000000..631a94652 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Test-ClusterPermissions.ps1 @@ -0,0 +1,86 @@ +<# + .SYNOPSIS + Determine if the cluster has the required permissions to the supplied server. + + .PARAMETER ServerObject + The server object on which to perform the test. +#> +function Test-ClusterPermissions +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '', Justification = 'Because the code throws based on an prior expression')] + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject + ) + + $clusterServiceName = 'NT SERVICE\ClusSvc' + $ntAuthoritySystemName = 'NT AUTHORITY\SYSTEM' + $availabilityGroupManagementPerms = @('Connect SQL', 'Alter Any Availability Group', 'View Server State') + $clusterPermissionsPresent = $false + + # Retrieve the SQL Server and Instance name from the server object + $sqlServer = $ServerObject.NetName + $sqlInstanceName = $ServerObject.ServiceName + + foreach ( $loginName in @( $clusterServiceName, $ntAuthoritySystemName ) ) + { + if ( $ServerObject.Logins[$loginName] -and -not $clusterPermissionsPresent ) + { + $testLoginEffectivePermissionsParams = @{ + ServerName = $sqlServer + InstanceName = $sqlInstanceName + LoginName = $loginName + Permissions = $availabilityGroupManagementPerms + } + + $clusterPermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + + if ( -not $clusterPermissionsPresent ) + { + switch ( $loginName ) + { + $clusterServiceName + { + Write-Verbose -Message ( $script:localizedData.ClusterLoginMissingRecommendedPermissions -f $loginName, ( $availabilityGroupManagementPerms -join ', ' ) ) -Verbose + } + + $ntAuthoritySystemName + { + Write-Verbose -Message ( $script:localizedData.ClusterLoginMissingPermissions -f $loginName, ( $availabilityGroupManagementPerms -join ', ' ) ) -Verbose + } + } + } + else + { + Write-Verbose -Message ( $script:localizedData.ClusterLoginPermissionsPresent -f $loginName ) -Verbose + } + } + elseif ( -not $clusterPermissionsPresent ) + { + switch ( $loginName ) + { + $clusterServiceName + { + Write-Verbose -Message ($script:localizedData.ClusterLoginMissingRecommendedPermissions -f $loginName, "Trying with '$ntAuthoritySystemName'.") -Verbose + } + + $ntAuthoritySystemName + { + Write-Verbose -Message ( $script:localizedData.ClusterLoginMissing -f $loginName, '' ) -Verbose + } + } + } + } + + # If neither 'NT SERVICE\ClusSvc' or 'NT AUTHORITY\SYSTEM' have the required permissions, throw an error. + if ( -not $clusterPermissionsPresent ) + { + throw ($script:localizedData.ClusterPermissionsMissing -f $sqlServer, $sqlInstanceName ) + } + + return $clusterPermissionsPresent +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Test-FeatureFlag.ps1 b/source/Modules/SqlServerDsc.Common/Public/Test-FeatureFlag.ps1 new file mode 100644 index 000000000..f35856f1d --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Test-FeatureFlag.ps1 @@ -0,0 +1,29 @@ +<# + .SYNOPSIS + Test if the specific feature flag should be enabled. + + .PARAMETER FeatureFlag + An array of feature flags that should be compared against. + + .PARAMETER TestFlag + The feature flag that is being check if it should be enabled. +#> +function Test-FeatureFlag +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter()] + [System.String[]] + $FeatureFlag, + + [Parameter(Mandatory = $true)] + [System.String] + $TestFlag + ) + + $flagEnabled = $FeatureFlag -and ($FeatureFlag -and $FeatureFlag.Contains($TestFlag)) + + return $flagEnabled +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Test-ImpersonatePermissions.ps1 b/source/Modules/SqlServerDsc.Common/Public/Test-ImpersonatePermissions.ps1 new file mode 100644 index 000000000..2d8c3198b --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Test-ImpersonatePermissions.ps1 @@ -0,0 +1,115 @@ +<# + .SYNOPSIS + Determine if the current login has impersonate permissions + + .PARAMETER ServerObject + The server object on which to perform the test. + + .PARAMETER SecurableName + If set then impersonate permission on this specific securable (e.g. login) is also checked. + +#> +function Test-ImpersonatePermissions +{ + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter()] + [System.String] + $SecurableName + ) + + # The impersonate any login permission only exists in SQL 2014 and above + $testLoginEffectivePermissionsParams = @{ + ServerName = $ServerObject.ComputerNamePhysicalNetBIOS + InstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('IMPERSONATE ANY LOGIN') + } + + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + + if ($impersonatePermissionsPresent) + { + Write-Verbose -Message ( 'The login "{0}" has impersonate any login permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose + return $impersonatePermissionsPresent + } + else + { + Write-Verbose -Message ( 'The login "{0}" does not have impersonate any login permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose + } + + # Check for sysadmin / control server permission which allows impersonation + $testLoginEffectivePermissionsParams = @{ + ServerName = $ServerObject.ComputerNamePhysicalNetBIOS + InstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('CONTROL SERVER') + } + + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + + if ($impersonatePermissionsPresent) + { + Write-Verbose -Message ( 'The login "{0}" has control server permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose + return $impersonatePermissionsPresent + } + else + { + Write-Verbose -Message ( 'The login "{0}" does not have control server permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose + } + + if (-not [System.String]::IsNullOrEmpty($SecurableName)) + { + # Check for login-specific impersonation permissions + $testLoginEffectivePermissionsParams = @{ + ServerName = $ServerObject.ComputerNamePhysicalNetBIOS + InstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('IMPERSONATE') + SecurableClass = 'LOGIN' + SecurableName = $SecurableName + } + + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + + if ($impersonatePermissionsPresent) + { + Write-Verbose -Message ( 'The login "{0}" has impersonate permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose + return $impersonatePermissionsPresent + } + else + { + Write-Verbose -Message ( 'The login "{0}" does not have impersonate permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose + } + + # Check for login-specific control permissions + $testLoginEffectivePermissionsParams = @{ + ServerName = $ServerObject.ComputerNamePhysicalNetBIOS + InstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('CONTROL') + SecurableClass = 'LOGIN' + SecurableName = $SecurableName + } + + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + + if ($impersonatePermissionsPresent) + { + Write-Verbose -Message ( 'The login "{0}" has control permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose + return $impersonatePermissionsPresent + } + else + { + Write-Verbose -Message ( 'The login "{0}" does not have control permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose + } + } + + Write-Verbose -Message ( 'The login "{0}" does not have any impersonate permissions required on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose + + return $impersonatePermissionsPresent +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Test-LoginEffectivePermissions.ps1 b/source/Modules/SqlServerDsc.Common/Public/Test-LoginEffectivePermissions.ps1 new file mode 100644 index 000000000..de60d067c --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Test-LoginEffectivePermissions.ps1 @@ -0,0 +1,119 @@ +<# + .SYNOPSIS + Impersonates a login and determines whether required permissions are present. + + .PARAMETER ServerName + String containing the host name of the SQL Server to connect to. + + .PARAMETER InstanceName + String containing the SQL Server Database Engine instance to connect to. + + .PARAMETER LoginName + String containing the login (user) which should be checked for a permission. + + .PARAMETER Permissions + This is a list that represents a SQL Server set of database permissions. + + .PARAMETER SecurableClass + String containing the class of permissions to test. It can be: + SERVER: A permission that is applicable against server objects. + LOGIN: A permission that is applicable against login objects. + + Default is 'SERVER'. + + .PARAMETER SecurableName + String containing the name of the object against which permissions exist, + e.g. if SecurableClass is LOGIN this is the name of a login permissions + may exist against. + + Default is $null. + + .NOTES + These SecurableClass are not yet in this module yet and so are not implemented: + 'APPLICATION ROLE', 'ASSEMBLY', 'ASYMMETRIC KEY', 'CERTIFICATE', + 'CONTRACT', 'DATABASE', 'ENDPOINT', 'FULLTEXT CATALOG', + 'MESSAGE TYPE', 'OBJECT', 'REMOTE SERVICE BINDING', 'ROLE', + 'ROUTE', 'SCHEMA', 'SERVICE', 'SYMMETRIC KEY', 'TYPE', 'USER', + 'XML SCHEMA COLLECTION' + +#> +function Test-LoginEffectivePermissions +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $ServerName, + + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $LoginName, + + [Parameter(Mandatory = $true)] + [System.String[]] + $Permissions, + + [Parameter()] + [ValidateSet('SERVER', 'LOGIN')] + [System.String] + $SecurableClass = 'SERVER', + + [Parameter()] + [System.String] + $SecurableName + ) + + # Assume the permissions are not present + $permissionsPresent = $false + + $invokeSqlDscQueryParameters = @{ + ServerName = $ServerName + InstanceName = $InstanceName + DatabaseName = 'master' + PassThru = $true + } + + if ( [System.String]::IsNullOrEmpty($SecurableName) ) + { + $queryToGetEffectivePermissionsForLogin = " + EXECUTE AS LOGIN = '$LoginName' + SELECT DISTINCT permission_name + FROM fn_my_permissions(null,'$SecurableClass') + REVERT + " + } + else + { + $queryToGetEffectivePermissionsForLogin = " + EXECUTE AS LOGIN = '$LoginName' + SELECT DISTINCT permission_name + FROM fn_my_permissions('$SecurableName','$SecurableClass') + REVERT + " + } + + Write-Verbose -Message ($script:localizedData.GetEffectivePermissionForLogin -f $LoginName, $InstanceName) -Verbose + + $loginEffectivePermissionsResult = Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $queryToGetEffectivePermissionsForLogin + $loginEffectivePermissions = $loginEffectivePermissionsResult.Tables.Rows.permission_name + + if ( $null -ne $loginEffectivePermissions ) + { + $loginMissingPermissions = Compare-Object -ReferenceObject $Permissions -DifferenceObject $loginEffectivePermissions | + Where-Object -FilterScript { $_.SideIndicator -ne '=>' } | + Select-Object -ExpandProperty InputObject + + if ( $loginMissingPermissions.Count -eq 0 ) + { + $permissionsPresent = $true + } + } + + return $permissionsPresent +} diff --git a/source/Modules/SqlServerDsc.Common/Public/Update-AvailabilityGroupReplica.ps1 b/source/Modules/SqlServerDsc.Common/Public/Update-AvailabilityGroupReplica.ps1 new file mode 100644 index 000000000..502487693 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/Public/Update-AvailabilityGroupReplica.ps1 @@ -0,0 +1,32 @@ +<# + .SYNOPSIS + Executes the alter method on an Availability Group Replica object. + + .PARAMETER AvailabilityGroupReplica + The Availability Group Replica object that must be altered. +#> +function Update-AvailabilityGroupReplica +{ + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.AvailabilityReplica] + $AvailabilityGroupReplica + ) + + try + { + $originalErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + $AvailabilityGroupReplica.Alter() + } + catch + { + $errorMessage = $script:localizedData.AlterAvailabilityGroupReplicaFailed -f $AvailabilityGroupReplica.Name + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + finally + { + $ErrorActionPreference = $originalErrorActionPreference + } +} diff --git a/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 deleted file mode 100644 index 02eb30cd1..000000000 --- a/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ /dev/null @@ -1,1987 +0,0 @@ -$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' - -Import-Module -Name $script:resourceHelperModulePath - -$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' - -<# - .SYNOPSIS - Copy folder structure using Robocopy. Every file and folder, including empty ones are copied. - - .PARAMETER Path - Source path to be copied. - - .PARAMETER DestinationPath - The path to the destination. -#> -function Copy-ItemWithRobocopy -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Path, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $DestinationPath - ) - - $quotedPath = '"{0}"' -f $Path - $quotedDestinationPath = '"{0}"' -f $DestinationPath - $robocopyExecutable = Get-Command -Name 'Robocopy.exe' -ErrorAction 'Stop' - - $robocopyArgumentSilent = '/njh /njs /ndl /nc /ns /nfl' - $robocopyArgumentCopySubDirectoriesIncludingEmpty = '/e' - $robocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource = '/purge' - - if ([System.Version]$robocopyExecutable.FileVersionInfo.ProductVersion -ge [System.Version]'6.3.9600.16384') - { - Write-Verbose -Message $script:localizedData.RobocopyUsingUnbufferedIo -Verbose - - $robocopyArgumentUseUnbufferedIO = '/J' - } - else - { - Write-Verbose -Message $script:localizedData.RobocopyNotUsingUnbufferedIo -Verbose - } - - $robocopyArgumentList = '{0} {1} {2} {3} {4} {5}' -f @( - $quotedPath, - $quotedDestinationPath, - $robocopyArgumentCopySubDirectoriesIncludingEmpty, - $robocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource, - $robocopyArgumentUseUnbufferedIO, - $robocopyArgumentSilent - ) - - $robocopyStartProcessParameters = @{ - FilePath = $robocopyExecutable.Name - ArgumentList = $robocopyArgumentList - } - - Write-Verbose -Message ($script:localizedData.RobocopyArguments -f $robocopyArgumentList) -Verbose - $robocopyProcess = Start-Process @robocopyStartProcessParameters -Wait -NoNewWindow -PassThru - - switch ($($robocopyProcess.ExitCode)) - { - { $_ -in 8, 16 } - { - $errorMessage = $script:localizedData.RobocopyErrorCopying -f $_ - New-InvalidOperationException -Message $errorMessage - } - - { $_ -gt 7 } - { - $errorMessage = $script:localizedData.RobocopyFailuresCopying -f $_ - New-InvalidResultException -Message $errorMessage - } - - 1 - { - Write-Verbose -Message $script:localizedData.RobocopySuccessful -Verbose - } - - 2 - { - Write-Verbose -Message $script:localizedData.RobocopyRemovedExtraFilesAtDestination -Verbose - } - - 3 - { - Write-Verbose -Message ( - '{0} {1}' -f $script:localizedData.RobocopySuccessful, $script:localizedData.RobocopyRemovedExtraFilesAtDestination - ) -Verbose - } - - { $_ -eq 0 -or $null -eq $_ } - { - Write-Verbose -Message $script:localizedData.RobocopyAllFilesPresent -Verbose - } - } -} - -<# - .SYNOPSIS - Connects to the source using the provided credentials and then uses - robocopy to download the installation media to a local temporary folder. - - .PARAMETER SourcePath - Source path to be copied. - - .PARAMETER SourceCredential - The credentials to access the SourcePath. - - .PARAMETER PassThru - If used, returns the destination path as string. - - .OUTPUTS - Returns the destination path (when used with the parameter PassThru). -#> -function Invoke-InstallationMediaCopy -{ - [CmdletBinding()] - [OutputType([System.String])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $SourcePath, - - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $SourceCredential, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $PassThru - ) - - Connect-UncPath -RemotePath $SourcePath -SourceCredential $SourceCredential - - $SourcePath = $SourcePath.TrimEnd('/\') - <# - Create a destination folder so the media files aren't written - to the root of the Temp folder. - #> - $serverName, $shareName, $leafs = ($SourcePath -replace '\\\\') -split '\\' - if ($leafs) - { - $mediaDestinationFolder = $leafs | Select-Object -Last 1 - } - else - { - $mediaDestinationFolder = New-Guid | Select-Object -ExpandProperty Guid - } - - $mediaDestinationPath = Join-Path -Path (Get-TemporaryFolder) -ChildPath $mediaDestinationFolder - - Write-Verbose -Message ($script:localizedData.RobocopyIsCopying -f $SourcePath, $mediaDestinationPath) -Verbose - Copy-ItemWithRobocopy -Path $SourcePath -DestinationPath $mediaDestinationPath - - Disconnect-UncPath -RemotePath $SourcePath - - if ($PassThru.IsPresent) - { - return $mediaDestinationPath - } -} - -<# - .SYNOPSIS - Connects to the UNC path provided in the parameter SourcePath. - Optionally connects using the provided credentials. - - .PARAMETER SourcePath - Source path to connect to. - - .PARAMETER SourceCredential - The credentials to access the path provided in SourcePath. - - .PARAMETER PassThru - If used, returns a MSFT_SmbMapping object that represents the newly - created SMB mapping. - - .OUTPUTS - Returns a MSFT_SmbMapping object that represents the newly created - SMB mapping (ony when used with parameter PassThru). -#> -function Connect-UncPath -{ - [CmdletBinding()] - [OutputType([Microsoft.Management.Infrastructure.CimInstance])] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $RemotePath, - - [Parameter()] - [System.Management.Automation.PSCredential] - $SourceCredential, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $PassThru - ) - - $newSmbMappingParameters = @{ - RemotePath = $RemotePath - } - - if ($PSBoundParameters.ContainsKey('SourceCredential')) - { - $newSmbMappingParameters['UserName'] = $SourceCredential.UserName - $newSmbMappingParameters['Password'] = $SourceCredential.GetNetworkCredential().Password - } - - $newSmbMappingResult = New-SmbMapping @newSmbMappingParameters - - if ($PassThru.IsPresent) - { - return $newSmbMappingResult - } -} - -<# - .SYNOPSIS - Disconnects from the UNC path provided in the parameter SourcePath. - - .PARAMETER SourcePath - Source path to disconnect from. -#> -function Disconnect-UncPath -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $RemotePath - ) - - Remove-SmbMapping -RemotePath $RemotePath -Force -} - -<# - .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' - - Write-Verbose -Message ($script:localizedData.StartSetupProcess -f $sqlSetupProcess.Id, $startProcessParameters.FilePath, $Timeout) -Verbose - - Wait-Process -InputObject $sqlSetupProcess -Timeout $Timeout -ErrorAction 'Stop' - - return $sqlSetupProcess.ExitCode -} - -<# - .SYNOPSIS - Connect to a SQL Server Database Engine and return the server object. - - .PARAMETER ServerName - String containing the host name of the SQL Server to connect to. - Default value is the current computer name. - - .PARAMETER InstanceName - String containing the SQL Server Database Engine instance to connect to. - Default value is 'MSSQLSERVER'. - - .PARAMETER SetupCredential - The credentials to use to impersonate a user when connecting to the - SQL Server Database Engine instance. If this parameter is left out, then - the current user will be used to connect to the SQL Server Database Engine - instance using Windows Integrated authentication. - - .PARAMETER LoginType - Specifies which type of logon credential should be used. The valid types - are 'WindowsUser' or 'SqlLogin'. Default value is 'WindowsUser' - If set to 'WindowsUser' then the it will impersonate using the Windows - login specified in the parameter SetupCredential. - If set to 'WindowsUser' then the it will impersonate using the native SQL - login specified in the parameter SetupCredential. - - .PARAMETER StatementTimeout - Set the query StatementTimeout in seconds. Default 600 seconds (10 minutes). - - .PARAMETER Encrypt - Specifies if encryption should be used. - - .EXAMPLE - Connect-SQL - - Connects to the default instance on the local server. - - .EXAMPLE - Connect-SQL -InstanceName 'MyInstance' - - Connects to the instance 'MyInstance' on the local server. - - .EXAMPLE - Connect-SQL ServerName 'sql.company.local' -InstanceName 'MyInstance' -ErrorAction 'Stop' - - Connects to the instance 'MyInstance' on the server 'sql.company.local'. -#> -function Connect-SQL -{ - [CmdletBinding(DefaultParameterSetName = 'SqlServer')] - param - ( - [Parameter(ParameterSetName = 'SqlServer')] - [Parameter(ParameterSetName = 'SqlServerWithCredential')] - [ValidateNotNull()] - [System.String] - $ServerName = (Get-ComputerName), - - [Parameter(ParameterSetName = 'SqlServer')] - [Parameter(ParameterSetName = 'SqlServerWithCredential')] - [ValidateNotNull()] - [System.String] - $InstanceName = 'MSSQLSERVER', - - [Parameter(ParameterSetName = 'SqlServerWithCredential', Mandatory = $true)] - [ValidateNotNull()] - [Alias('SetupCredential', 'DatabaseCredential')] - [System.Management.Automation.PSCredential] - $Credential, - - [Parameter(ParameterSetName = 'SqlServerWithCredential')] - [ValidateSet('WindowsUser', 'SqlLogin')] - [System.String] - $LoginType = 'WindowsUser', - - [Parameter()] - [ValidateSet('tcp', 'np', 'lpc')] - [System.String] - $Protocol, - - [Parameter()] - [System.UInt16] - $Port, - - [Parameter()] - [ValidateNotNull()] - [System.Int32] - $StatementTimeout = 600, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $Encrypt - ) - - Import-SqlDscPreferredModule - - <# - Build the connection string in the format: [protocol:]hostname[\instance][,port] - Examples: - - ServerName (default instance, no protocol/port) - - ServerName\Instance (named instance) - - tcp:ServerName (default instance with protocol) - - tcp:ServerName\Instance (named instance with protocol) - - ServerName,1433 (default instance with port) - - ServerName\Instance,50200 (named instance with port) - - tcp:ServerName,1433 (default instance with protocol and port) - - tcp:ServerName\Instance,50200 (named instance with protocol and port) - #> - if ($InstanceName -eq 'MSSQLSERVER') - { - $databaseEngineInstance = $ServerName - } - else - { - $databaseEngineInstance = '{0}\{1}' -f $ServerName, $InstanceName - } - - # Append port if specified - if ($PSBoundParameters.ContainsKey('Port')) - { - $databaseEngineInstance = '{0},{1}' -f $databaseEngineInstance, $Port - } - - # Prepend protocol if specified - if ($PSBoundParameters.ContainsKey('Protocol')) - { - $databaseEngineInstance = '{0}:{1}' -f $Protocol, $databaseEngineInstance - } - - $sqlServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' - $sqlConnectionContext = $sqlServerObject.ConnectionContext - $sqlConnectionContext.ServerInstance = $databaseEngineInstance - $sqlConnectionContext.StatementTimeout = $StatementTimeout - $sqlConnectionContext.ConnectTimeout = $StatementTimeout - $sqlConnectionContext.ApplicationName = 'SqlServerDsc' - - if ($Encrypt.IsPresent) - { - $sqlConnectionContext.EncryptConnection = $true - } - - if ($PSCmdlet.ParameterSetName -eq 'SqlServer') - { - <# - This is only used for verbose messaging and not for the connection - string since this is using Integrated Security=true (SSPI). - #> - $connectUserName = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name - - Write-Verbose -Message ( - $script:localizedData.ConnectingUsingIntegrated -f $connectUsername - ) - } - else - { - $connectUserName = $Credential.UserName - - Write-Verbose -Message ( - $script:localizedData.ConnectingUsingImpersonation -f $connectUsername, $LoginType - ) - - if ($LoginType -eq 'SqlLogin') - { - $sqlConnectionContext.LoginSecure = $false - $sqlConnectionContext.Login = $connectUserName - $sqlConnectionContext.SecurePassword = $Credential.Password - } - - if ($LoginType -eq 'WindowsUser') - { - $sqlConnectionContext.LoginSecure = $true - $sqlConnectionContext.ConnectAsUser = $true - $sqlConnectionContext.ConnectAsUserName = $connectUserName - $sqlConnectionContext.ConnectAsUserPassword = $Credential.GetNetworkCredential().Password - } - } - - try - { - $onlineStatus = 'Online' - $connectTimer = [System.Diagnostics.StopWatch]::StartNew() - $sqlConnectionContext.Connect() - - <# - The addition of the ConnectTimeout property to the ConnectionContext will force the - Connect() method to block until successful. THe SMO object's Status property may not - report 'Online' immediately even though the Connect() was successful. The loop is to - ensure the SMO's Status property was been updated. - #> - $sleepInSeconds = 2 - do - { - $instanceStatus = $sqlServerObject.Status - if ([System.String]::IsNullOrEmpty($instanceStatus)) - { - $instanceStatus = 'Unknown' - } - else - { - # Property Status is of type Enum ServerStatus, we return the string equivalent. - $instanceStatus = $instanceStatus.ToString() - } - - if ($instanceStatus -eq $onlineStatus) - { - break - } - - Write-Debug -Message ( - $script:localizedData.WaitForDatabaseEngineInstanceStatus -f $instanceStatus, $onlineStatus, $sleepInSeconds - ) - - Start-Sleep -Seconds $sleepInSeconds - $sqlServerObject.Refresh() - } while ($connectTimer.Elapsed.TotalSeconds -lt $StatementTimeout) - - if ($instanceStatus -match '^Online$') - { - Write-Verbose -Message ( - $script:localizedData.ConnectedToDatabaseEngineInstance -f $databaseEngineInstance - ) - - return $sqlServerObject - } - else - { - $errorMessage = $script:localizedData.DatabaseEngineInstanceNotOnline -f @( - $databaseEngineInstance, - $instanceStatus - ) - - $invalidOperationException = New-Object -TypeName 'InvalidOperationException' -ArgumentList @($errorMessage) - - $newObjectParameters = @{ - TypeName = 'System.Management.Automation.ErrorRecord' - ArgumentList = @( - $invalidOperationException, - 'CS0001', - 'InvalidOperation', - $databaseEngineInstance - ) - } - - $errorRecordToThrow = New-Object @newObjectParameters - - Write-Error -ErrorRecord $errorRecordToThrow - } - } - catch - { - $errorMessage = $script:localizedData.FailedToConnectToDatabaseEngineInstance -f $databaseEngineInstance - - $invalidOperationException = New-Object -TypeName 'InvalidOperationException' -ArgumentList @($errorMessage, $_.Exception) - - $newObjectParameters = @{ - TypeName = 'System.Management.Automation.ErrorRecord' - ArgumentList = @( - $invalidOperationException, - 'CS0002', - 'InvalidOperation', - $databaseEngineInstance - ) - } - - $errorRecordToThrow = New-Object @newObjectParameters - - Write-Error -ErrorRecord $errorRecordToThrow - } - finally - { - $connectTimer.Stop() - <# - Connect will ensure we actually can connect, but we need to disconnect - from the session so we don't have anything hanging. If we need run a - method on the returned $sqlServerObject it will automatically open a - new session and then close, therefore we don't need to keep this - session open. - #> - $sqlConnectionContext.Disconnect() - } -} - -<# - .SYNOPSIS - Connect to a SQL Server Analysis Service and return the server object. - - .PARAMETER ServerName - String containing the host name of the SQL Server to connect to. - - .PARAMETER InstanceName - String containing the SQL Server Analysis Service instance to connect to. - - .PARAMETER SetupCredential - PSCredential object with the credentials to use to impersonate a user when - connecting. If this is not provided then the current user will be used to - connect to the SQL Server Analysis Service instance. -#> -function Connect-SQLAnalysis -{ - [CmdletBinding()] - param - ( - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $ServerName = (Get-ComputerName), - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $InstanceName = 'MSSQLSERVER', - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.Management.Automation.PSCredential] - [System.Management.Automation.Credential()] - $SetupCredential, - - [Parameter()] - [System.String[]] - $FeatureFlag - ) - - if ($InstanceName -eq 'MSSQLSERVER') - { - $analysisServiceInstance = $ServerName - } - else - { - $analysisServiceInstance = "$ServerName\$InstanceName" - } - - if ($SetupCredential) - { - $userName = $SetupCredential.UserName - $password = $SetupCredential.GetNetworkCredential().Password - - $analysisServicesDataSource = "Data Source=$analysisServiceInstance;User ID=$userName;Password=$password" - } - else - { - $analysisServicesDataSource = "Data Source=$analysisServiceInstance" - } - - try - { - if ((Test-FeatureFlag -FeatureFlag $FeatureFlag -TestFlag 'AnalysisServicesConnection')) - { - Import-SqlDscPreferredModule - - $analysisServicesObject = New-Object -TypeName 'Microsoft.AnalysisServices.Server' - - if ($analysisServicesObject) - { - $analysisServicesObject.Connect($analysisServicesDataSource) - } - - if ((-not $analysisServicesObject) -or ($analysisServicesObject -and $analysisServicesObject.Connected -eq $false)) - { - $errorMessage = $script:localizedData.FailedToConnectToAnalysisServicesInstance -f $analysisServiceInstance - - New-InvalidOperationException -Message $errorMessage - } - else - { - Write-Verbose -Message ($script:localizedData.ConnectedToAnalysisServicesInstance -f $analysisServiceInstance) -Verbose - } - } - else - { - $null = Import-Assembly -Name 'Microsoft.AnalysisServices' -LoadWithPartialName - - $analysisServicesObject = New-Object -TypeName 'Microsoft.AnalysisServices.Server' - - if ($analysisServicesObject) - { - $analysisServicesObject.Connect($analysisServicesDataSource) - } - else - { - $errorMessage = $script:localizedData.FailedToConnectToAnalysisServicesInstance -f $analysisServiceInstance - - New-InvalidOperationException -Message $errorMessage - } - - Write-Verbose -Message ($script:localizedData.ConnectedToAnalysisServicesInstance -f $analysisServiceInstance) -Verbose - } - } - catch - { - $errorMessage = $script:localizedData.FailedToConnectToAnalysisServicesInstance -f $analysisServiceInstance - - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - - return $analysisServicesObject -} - -<# - .SYNOPSIS - Imports the assembly into the session. - - .DESCRIPTION - Imports the assembly into the session and returns a reference to the - assembly. - - .PARAMETER Name - Specifies the name of the assembly to load. - - .PARAMETER LoadWithPartialName - Specifies if the imported assembly should be the first found in GAC, - regardless of version. - - .OUTPUTS - [System.Reflection.Assembly] - - Returns a reference to the assembly object. - - .EXAMPLE - Import-Assembly -Name "Microsoft.SqlServer.ConnectionInfo, Version=$SqlMajorVersion.0.0.0, Culture=neutral, PublicKeyToken=89845dcd8080cc91" - - .EXAMPLE - Import-Assembly -Name 'Microsoft.AnalysisServices' -LoadWithPartialName - - .NOTES - This should normally work using Import-Module and New-Object instead of - using the method [System.Reflection.Assembly]::Load(). But due to a - missing assembly in the module SqlServer this is still needed. - - Import-Module SqlServer - $connectionInfo = New-Object -TypeName 'Microsoft.SqlServer.Management.Common.ServerConnection' -ArgumentList @('testclu01a\SQL2014') - # Missing assembly 'Microsoft.SqlServer.Rmo' in module SqlServer prevents this call from working. - $replication = New-Object -TypeName 'Microsoft.SqlServer.Replication.ReplicationServer' -ArgumentList @($connectionInfo) -#> -function Import-Assembly -{ - [CmdletBinding()] - [OutputType([System.Reflection.Assembly])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $Name, - - [Parameter()] - [System.Management.Automation.SwitchParameter] - $LoadWithPartialName - ) - - try - { - if ($LoadWithPartialName.IsPresent) - { - $assemblyInformation = [System.Reflection.Assembly]::LoadWithPartialName($Name) - } - else - { - $assemblyInformation = [System.Reflection.Assembly]::Load($Name) - } - - Write-Verbose -Message ( - $script:localizedData.LoadedAssembly -f $assemblyInformation.FullName - ) - } - catch - { - $errorMessage = $script:localizedData.FailedToLoadAssembly -f $Name - - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - - return $assemblyInformation -} - - -<# - .SYNOPSIS - Returns the major SQL version for the specific instance. - - .PARAMETER InstanceName - String containing the name of the SQL instance to be configured. Default - value is 'MSSQLSERVER'. - - .OUTPUTS - System.UInt16. Returns the SQL Server major version number. -#> -function Get-SqlInstanceMajorVersion -{ - [CmdletBinding()] - [OutputType([System.UInt16])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $InstanceName - ) - - $sqlInstanceId = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL').$InstanceName - $sqlVersion = (Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$sqlInstanceId\Setup").Version - - if (-not $sqlVersion) - { - $errorMessage = $script:localizedData.SqlServerVersionIsInvalid -f $InstanceName - New-InvalidResultException -Message $errorMessage - } - - [System.UInt16] $sqlMajorVersionNumber = $sqlVersion.Split('.')[0] - - return $sqlMajorVersionNumber -} - -<# - .SYNOPSIS - Restarts a SQL Server instance and associated services - - .PARAMETER ServerName - Hostname of the SQL Server to be configured - - .PARAMETER InstanceName - Name of the SQL instance to be configured. Default is 'MSSQLSERVER' - - .PARAMETER Timeout - Timeout value for restarting the SQL services. The default value is 120 seconds. - - .PARAMETER SkipClusterCheck - If cluster check should be skipped. If this is present no connection - is made to the instance to check if the instance is on a cluster. - - This need to be used for some resource, for example for the SqlServerNetwork - resource when it's used to enable a disable protocol. - - .PARAMETER SkipWaitForOnline - If this is present no connection is made to the instance to check if the - instance is online. - - This need to be used for some resource, for example for the SqlServerNetwork - resource when it's used to disable protocol. - - .PARAMETER OwnerNode - Specifies a list of owner nodes names of a cluster groups. If the SQL Server - instance is a Failover Cluster instance then the cluster group will only - be taken offline and back online when the owner of the cluster group is - one of the nodes specified in this list. These node names specified in this - parameter must match the Owner property of the cluster resource, for example - @('sqltest10', 'SQLTEST11'). The names are case-insensitive. - If this parameter is not specified the cluster group will be taken offline - and back online regardless of owner. - - .EXAMPLE - Restart-SqlService -ServerName localhost - - .EXAMPLE - Restart-SqlService -ServerName localhost -InstanceName 'NamedInstance' - - .EXAMPLE - Restart-SqlService -ServerName localhost -InstanceName 'NamedInstance' -SkipClusterCheck -SkipWaitForOnline - - .EXAMPLE - Restart-SqlService -ServerName CLU01 -Timeout 300 - - .EXAMPLE - Restart-SqlService -ServerName CLU01 -Timeout 300 -OwnerNode 'testclu10' -#> -function Restart-SqlService -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $ServerName, - - [Parameter()] - [System.String] - $InstanceName = 'MSSQLSERVER', - - [Parameter()] - [System.UInt32] - $Timeout = 120, - - [Parameter()] - [Switch] - $SkipClusterCheck, - - [Parameter()] - [Switch] - $SkipWaitForOnline, - - [Parameter()] - [System.String[]] - $OwnerNode - ) - - $restartWindowsService = $true - - # Check if a cluster, otherwise assume that a Windows service should be restarted. - if (-not $SkipClusterCheck.IsPresent) - { - ## Connect to the instance - $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' - - if ($serverObject.IsClustered) - { - # Make sure Windows service is not restarted outside of the cluster. - $restartWindowsService = $false - - $restartSqlClusterServiceParameters = @{ - InstanceName = $serverObject.ServiceName - } - - if ($PSBoundParameters.ContainsKey('Timeout')) - { - $restartSqlClusterServiceParameters['Timeout'] = $Timeout - } - - if ($PSBoundParameters.ContainsKey('OwnerNode')) - { - $restartSqlClusterServiceParameters['OwnerNode'] = $OwnerNode - } - - Restart-SqlClusterService @restartSqlClusterServiceParameters - } - } - - if ($restartWindowsService) - { - if ($InstanceName -eq 'MSSQLSERVER') - { - $serviceName = 'MSSQLSERVER' - } - else - { - $serviceName = 'MSSQL${0}' -f $InstanceName - } - - Write-Verbose -Message ($script:localizedData.GetServiceInformation -f 'SQL Server') -Verbose - - $sqlService = Get-Service -Name $serviceName - - <# - Get all dependent services that are running. - There are scenarios where an automatic service is stopped and should not be restarted automatically. - #> - $agentService = $sqlService.DependentServices | - Where-Object -FilterScript { $_.Status -eq 'Running' } - - # Restart the SQL Server service - Write-Verbose -Message ($script:localizedData.RestartService -f 'SQL Server') -Verbose - $sqlService | - Restart-Service -Force - - # Start dependent services - $agentService | - ForEach-Object -Process { - Write-Verbose -Message ($script:localizedData.StartingDependentService -f $_.DisplayName) -Verbose - $_ | Start-Service - } - } - - Write-Verbose -Message ($script:localizedData.WaitingInstanceTimeout -f $ServerName, $InstanceName, $Timeout) -Verbose - - if (-not $SkipWaitForOnline.IsPresent) - { - $connectTimer = [System.Diagnostics.StopWatch]::StartNew() - - $connectSqlError = $null - - do - { - # This call, if it fails, will take between ~9-10 seconds to return. - $testConnectionServerObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'SilentlyContinue' -ErrorVariable 'connectSqlError' - - # Make sure we have an SMO object to test Status - if ($testConnectionServerObject) - { - if ($testConnectionServerObject.Status -eq 'Online') - { - break - } - } - - # Waiting 2 seconds to not hammer the SQL Server instance. - Start-Sleep -Seconds 2 - } until ($connectTimer.Elapsed.TotalSeconds -ge $Timeout) - - $connectTimer.Stop() - - # Was the timeout period reach before able to connect to the SQL Server instance? - if (-not $testConnectionServerObject -or $testConnectionServerObject.Status -ne 'Online') - { - $errorMessage = $script:localizedData.FailedToConnectToInstanceTimeout -f @( - $ServerName, - $InstanceName, - $Timeout - ) - - $newInvalidOperationExceptionParameters = @{ - Message = $errorMessage - } - - if ($connectSqlError) - { - $newInvalidOperationExceptionParameters.ErrorRecord = $connectSqlError[$connectSqlError.Count - 1] - } - - New-InvalidOperationException @newInvalidOperationExceptionParameters - } - } -} - -<# - .SYNOPSIS - Restarts a SQL Server cluster instance and associated services - - .PARAMETER InstanceName - Specifies the instance name that matches a SQL Server MSCluster_Resource - property .PrivateProperties.InstanceName. - - .PARAMETER Timeout - Timeout value for restarting the SQL services. The default value is 120 seconds. - - .PARAMETER OwnerNode - Specifies a list of owner nodes names of a cluster groups. If the SQL Server - instance is a Failover Cluster instance then the cluster group will only - be taken offline and back online when the owner of the cluster group is - one of the nodes specified in this list. These node names specified in this - parameter must match the Owner property of the cluster resource, for example - @('sqltest10', 'SQLTEST11'). The names are case-insensitive. - If this parameter is not specified the cluster group will be taken offline - and back online regardless of owner. -#> -function Restart-SqlClusterService -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $InstanceName, - - [Parameter()] - [System.UInt32] - $Timeout = 120, - - [Parameter()] - [System.String[]] - $OwnerNode - ) - - # Get the cluster resources - Write-Verbose -Message ($script:localizedData.GetSqlServerClusterResources) -Verbose - - $sqlService = Get-CimInstance -Namespace 'root/MSCluster' -ClassName 'MSCluster_Resource' -Filter "Type = 'SQL Server'" | - Where-Object -FilterScript { - $_.PrivateProperties.InstanceName -eq $InstanceName -and $_.State -eq 2 - } - - # If the cluster resource is found and online then continue. - if ($sqlService) - { - $isOwnerOfClusterResource = $true - - if ($PSBoundParameters.ContainsKey('OwnerNode') -and $sqlService.OwnerNode -notin $OwnerNode) - { - $isOwnerOfClusterResource = $false - } - - if ($isOwnerOfClusterResource) - { - Write-Verbose -Message ($script:localizedData.GetSqlAgentClusterResource) -Verbose - - $agentService = $sqlService | - Get-CimAssociatedInstance -ResultClassName MSCluster_Resource | - Where-Object -FilterScript { - $_.Type -eq 'SQL Server Agent' -and $_.State -eq 2 - } - - # Build a listing of resources being acted upon - $resourceNames = @($sqlService.Name, ($agentService | - Select-Object -ExpandProperty Name)) -join "', '" - - # Stop the SQL Server and dependent resources - Write-Verbose -Message ($script:localizedData.BringClusterResourcesOffline -f $resourceNames) -Verbose - - $sqlService | - Invoke-CimMethod -MethodName TakeOffline -Arguments @{ - Timeout = $Timeout - } - - # Start the SQL server resource - Write-Verbose -Message ($script:localizedData.BringSqlServerClusterResourcesOnline) -Verbose - - $sqlService | - Invoke-CimMethod -MethodName BringOnline -Arguments @{ - Timeout = $Timeout - } - - # Start the SQL Agent resource - if ($agentService) - { - if ($PSBoundParameters.ContainsKey('OwnerNode') -and $agentService.OwnerNode -notin $OwnerNode) - { - $isOwnerOfClusterResource = $false - } - - if ($isOwnerOfClusterResource) - { - Write-Verbose -Message ($script:localizedData.BringSqlServerAgentClusterResourcesOnline) -Verbose - - $agentService | - Invoke-CimMethod -MethodName BringOnline -Arguments @{ - Timeout = $Timeout - } - } - else - { - Write-Verbose -Message ( - $script:localizedData.NotOwnerOfClusterResource -f (Get-ComputerName), $agentService.Name, $agentService.OwnerNode - ) -Verbose - } - } - } - else - { - Write-Verbose -Message ( - $script:localizedData.NotOwnerOfClusterResource -f (Get-ComputerName), $sqlService.Name, $sqlService.OwnerNode - ) -Verbose - } - } - else - { - Write-Warning -Message ($script:localizedData.ClusterResourceNotFoundOrOffline -f $InstanceName) - } -} - -<# - .SYNOPSIS - Executes the alter method on an Availability Group Replica object. - - .PARAMETER AvailabilityGroupReplica - The Availability Group Replica object that must be altered. -#> -function Update-AvailabilityGroupReplica -{ - param - ( - [Parameter(Mandatory = $true)] - [Microsoft.SqlServer.Management.Smo.AvailabilityReplica] - $AvailabilityGroupReplica - ) - - try - { - $originalErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'Stop' - $AvailabilityGroupReplica.Alter() - } - catch - { - $errorMessage = $script:localizedData.AlterAvailabilityGroupReplicaFailed -f $AvailabilityGroupReplica.Name - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - finally - { - $ErrorActionPreference = $originalErrorActionPreference - } -} - -<# - .SYNOPSIS - Impersonates a login and determines whether required permissions are present. - - .PARAMETER ServerName - String containing the host name of the SQL Server to connect to. - - .PARAMETER InstanceName - String containing the SQL Server Database Engine instance to connect to. - - .PARAMETER LoginName - String containing the login (user) which should be checked for a permission. - - .PARAMETER Permissions - This is a list that represents a SQL Server set of database permissions. - - .PARAMETER SecurableClass - String containing the class of permissions to test. It can be: - SERVER: A permission that is applicable against server objects. - LOGIN: A permission that is applicable against login objects. - - Default is 'SERVER'. - - .PARAMETER SecurableName - String containing the name of the object against which permissions exist, - e.g. if SecurableClass is LOGIN this is the name of a login permissions - may exist against. - - Default is $null. - - .NOTES - These SecurableClass are not yet in this module yet and so are not implemented: - 'APPLICATION ROLE', 'ASSEMBLY', 'ASYMMETRIC KEY', 'CERTIFICATE', - 'CONTRACT', 'DATABASE', 'ENDPOINT', 'FULLTEXT CATALOG', - 'MESSAGE TYPE', 'OBJECT', 'REMOTE SERVICE BINDING', 'ROLE', - 'ROUTE', 'SCHEMA', 'SERVICE', 'SYMMETRIC KEY', 'TYPE', 'USER', - 'XML SCHEMA COLLECTION' - -#> -function Test-LoginEffectivePermissions -{ - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $ServerName, - - [Parameter(Mandatory = $true)] - [System.String] - $InstanceName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $LoginName, - - [Parameter(Mandatory = $true)] - [System.String[]] - $Permissions, - - [Parameter()] - [ValidateSet('SERVER', 'LOGIN')] - [System.String] - $SecurableClass = 'SERVER', - - [Parameter()] - [System.String] - $SecurableName - ) - - # Assume the permissions are not present - $permissionsPresent = $false - - $invokeSqlDscQueryParameters = @{ - ServerName = $ServerName - InstanceName = $InstanceName - DatabaseName = 'master' - PassThru = $true - } - - if ( [System.String]::IsNullOrEmpty($SecurableName) ) - { - $queryToGetEffectivePermissionsForLogin = " - EXECUTE AS LOGIN = '$LoginName' - SELECT DISTINCT permission_name - FROM fn_my_permissions(null,'$SecurableClass') - REVERT - " - } - else - { - $queryToGetEffectivePermissionsForLogin = " - EXECUTE AS LOGIN = '$LoginName' - SELECT DISTINCT permission_name - FROM fn_my_permissions('$SecurableName','$SecurableClass') - REVERT - " - } - - Write-Verbose -Message ($script:localizedData.GetEffectivePermissionForLogin -f $LoginName, $InstanceName) -Verbose - - $loginEffectivePermissionsResult = Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $queryToGetEffectivePermissionsForLogin - $loginEffectivePermissions = $loginEffectivePermissionsResult.Tables.Rows.permission_name - - if ( $null -ne $loginEffectivePermissions ) - { - $loginMissingPermissions = Compare-Object -ReferenceObject $Permissions -DifferenceObject $loginEffectivePermissions | - Where-Object -FilterScript { $_.SideIndicator -ne '=>' } | - Select-Object -ExpandProperty InputObject - - if ( $loginMissingPermissions.Count -eq 0 ) - { - $permissionsPresent = $true - } - } - - return $permissionsPresent -} - -<# - .SYNOPSIS - Determine if the seeding mode of the specified availability group is automatic. - - .PARAMETER ServerName - The hostname of the server that hosts the SQL instance. - - .PARAMETER InstanceName - The name of the SQL instance that hosts the availability group. - - .PARAMETER AvailabilityGroupName - The name of the availability group to check. - - .PARAMETER AvailabilityReplicaName - The name of the availability replica to check. -#> -function Test-AvailabilityReplicaSeedingModeAutomatic -{ - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $ServerName, - - [Parameter(Mandatory = $true)] - [System.String] - $InstanceName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $AvailabilityGroupName, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $AvailabilityReplicaName - ) - - # Assume automatic seeding is disabled by default - $availabilityReplicaSeedingModeAutomatic = $false - - $serverObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' - - # Only check the seeding mode if this is SQL 2016 or newer - if ( $serverObject.Version -ge 13 ) - { - $invokeSqlDscQueryParameters = @{ - ServerName = $ServerName - InstanceName = $InstanceName - DatabaseName = 'master' - PassThru = $true - } - - $queryToGetSeedingMode = " - SELECT seeding_mode_desc - FROM sys.availability_replicas ar - INNER JOIN sys.availability_groups ag ON ar.group_id = ag.group_id - WHERE ag.name = '$AvailabilityGroupName' - AND ar.replica_server_name = '$AvailabilityReplicaName' - " - $seedingModeResults = Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $queryToGetSeedingMode - $seedingMode = $seedingModeResults.Tables.Rows.seeding_mode_desc - - if ( $seedingMode -eq 'Automatic' ) - { - $availabilityReplicaSeedingModeAutomatic = $true - } - } - - return $availabilityReplicaSeedingModeAutomatic -} - -<# - .SYNOPSIS - Get the server object of the primary replica of the specified availability group. - - .PARAMETER ServerObject - The current server object connection. - - .PARAMETER AvailabilityGroup - The availability group object used to find the primary replica server name. -#> -function Get-PrimaryReplicaServerObject -{ - param - ( - [Parameter(Mandatory = $true)] - [Microsoft.SqlServer.Management.Smo.Server] - $ServerObject, - - [Parameter(Mandatory = $true)] - [Microsoft.SqlServer.Management.Smo.AvailabilityGroup] - $AvailabilityGroup - ) - - $primaryReplicaServerObject = $serverObject - - # Determine if we're connected to the primary replica - if ( ( $AvailabilityGroup.PrimaryReplicaServerName -ne $serverObject.DomainInstanceName ) -and ( -not [System.String]::IsNullOrEmpty($AvailabilityGroup.PrimaryReplicaServerName) ) ) - { - $primaryReplicaServerObject = Connect-SQL -ServerName $AvailabilityGroup.PrimaryReplicaServerName -ErrorAction 'Stop' - } - - return $primaryReplicaServerObject -} - -<# - .SYNOPSIS - Determine if the current login has impersonate permissions - - .PARAMETER ServerObject - The server object on which to perform the test. - - .PARAMETER SecurableName - If set then impersonate permission on this specific securable (e.g. login) is also checked. - -#> -function Test-ImpersonatePermissions -{ - param - ( - [Parameter(Mandatory = $true)] - [Microsoft.SqlServer.Management.Smo.Server] - $ServerObject, - - [Parameter()] - [System.String] - $SecurableName - ) - - # The impersonate any login permission only exists in SQL 2014 and above - $testLoginEffectivePermissionsParams = @{ - ServerName = $ServerObject.ComputerNamePhysicalNetBIOS - InstanceName = $ServerObject.ServiceName - LoginName = $ServerObject.ConnectionContext.TrueLogin - Permissions = @('IMPERSONATE ANY LOGIN') - } - - $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams - - if ($impersonatePermissionsPresent) - { - Write-Verbose -Message ( 'The login "{0}" has impersonate any login permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.SInstanceName ) -Verbose - return $impersonatePermissionsPresent - } - else - { - Write-Verbose -Message ( 'The login "{0}" does not have impersonate any login permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose - } - - # Check for sysadmin / control server permission which allows impersonation - $testLoginEffectivePermissionsParams = @{ - ServerName = $ServerObject.ComputerNamePhysicalNetBIOS - InstanceName = $ServerObject.ServiceName - LoginName = $ServerObject.ConnectionContext.TrueLogin - Permissions = @('CONTROL SERVER') - } - - $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams - - if ($impersonatePermissionsPresent) - { - Write-Verbose -Message ( 'The login "{0}" has control server permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose - return $impersonatePermissionsPresent - } - else - { - Write-Verbose -Message ( 'The login "{0}" does not have control server permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose - } - - if (-not [System.String]::IsNullOrEmpty($SecurableName)) - { - # Check for login-specific impersonation permissions - $testLoginEffectivePermissionsParams = @{ - ServerName = $ServerObject.ComputerNamePhysicalNetBIOS - InstanceName = $ServerObject.ServiceName - LoginName = $ServerObject.ConnectionContext.TrueLogin - Permissions = @('IMPERSONATE') - SecurableClass = 'LOGIN' - SecurableName = $SecurableName - } - - $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams - - if ($impersonatePermissionsPresent) - { - Write-Verbose -Message ( 'The login "{0}" has impersonate permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose - return $impersonatePermissionsPresent - } - else - { - Write-Verbose -Message ( 'The login "{0}" does not have impersonate permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose - } - - # Check for login-specific control permissions - $testLoginEffectivePermissionsParams = @{ - ServerName = $ServerObject.ComputerNamePhysicalNetBIOS - InstanceName = $ServerObject.ServiceName - LoginName = $ServerObject.ConnectionContext.TrueLogin - Permissions = @('CONTROL') - SecurableClass = 'LOGIN' - SecurableName = $SecurableName - } - - $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams - - if ($impersonatePermissionsPresent) - { - Write-Verbose -Message ( 'The login "{0}" has control permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose - return $impersonatePermissionsPresent - } - else - { - Write-Verbose -Message ( 'The login "{0}" does not have control permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName, $SecurableName ) -Verbose - } - } - - Write-Verbose -Message ( 'The login "{0}" does not have any impersonate permissions required on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.ServerName, $testLoginEffectivePermissionsParams.InstanceName ) -Verbose - - return $impersonatePermissionsPresent -} - -<# - .SYNOPSIS - Takes a SQL Instance name in the format of 'Server\Instance' and splits - it into a hash table prepared to be passed into Connect-SQL. - - .PARAMETER FullSqlInstanceName - The full SQL instance name string to be split. - - .OUTPUTS - Hash table with the properties ServerName and InstanceName. -#> -function Split-FullSqlInstanceName -{ - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $FullSqlInstanceName - ) - - $sqlServer, $sqlInstanceName = $FullSqlInstanceName.Split('\') - - if ( [System.String]::IsNullOrEmpty($sqlInstanceName) ) - { - $sqlInstanceName = 'MSSQLSERVER' - } - - return @{ - ServerName = $sqlServer - InstanceName = $sqlInstanceName - } -} - -<# - .SYNOPSIS - Determine if the cluster has the required permissions to the supplied server. - - .PARAMETER ServerObject - The server object on which to perform the test. -#> -function Test-ClusterPermissions -{ - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '', Justification = 'Because the code throws based on an prior expression')] - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter(Mandatory = $true)] - [Microsoft.SqlServer.Management.Smo.Server] - $ServerObject - ) - - $clusterServiceName = 'NT SERVICE\ClusSvc' - $ntAuthoritySystemName = 'NT AUTHORITY\SYSTEM' - $availabilityGroupManagementPerms = @('Connect SQL', 'Alter Any Availability Group', 'View Server State') - $clusterPermissionsPresent = $false - - # Retrieve the SQL Server and Instance name from the server object - $sqlServer = $ServerObject.NetName - $sqlInstanceName = $ServerObject.ServiceName - - foreach ( $loginName in @( $clusterServiceName, $ntAuthoritySystemName ) ) - { - if ( $ServerObject.Logins[$loginName] -and -not $clusterPermissionsPresent ) - { - $testLoginEffectivePermissionsParams = @{ - ServerName = $sqlServer - InstanceName = $sqlInstanceName - LoginName = $loginName - Permissions = $availabilityGroupManagementPerms - } - - $clusterPermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams - - if ( -not $clusterPermissionsPresent ) - { - switch ( $loginName ) - { - $clusterServiceName - { - Write-Verbose -Message ( $script:localizedData.ClusterLoginMissingRecommendedPermissions -f $loginName, ( $availabilityGroupManagementPerms -join ', ' ) ) -Verbose - } - - $ntAuthoritySystemName - { - Write-Verbose -Message ( $script:localizedData.ClusterLoginMissingPermissions -f $loginName, ( $availabilityGroupManagementPerms -join ', ' ) ) -Verbose - } - } - } - else - { - Write-Verbose -Message ( $script:localizedData.ClusterLoginPermissionsPresent -f $loginName ) -Verbose - } - } - elseif ( -not $clusterPermissionsPresent ) - { - switch ( $loginName ) - { - $clusterServiceName - { - Write-Verbose -Message ($script:localizedData.ClusterLoginMissingRecommendedPermissions -f $loginName, "Trying with '$ntAuthoritySystemName'.") -Verbose - } - - $ntAuthoritySystemName - { - Write-Verbose -Message ( $script:localizedData.ClusterLoginMissing -f $loginName, '' ) -Verbose - } - } - } - } - - # If neither 'NT SERVICE\ClusSvc' or 'NT AUTHORITY\SYSTEM' have the required permissions, throw an error. - if ( -not $clusterPermissionsPresent ) - { - throw ($script:localizedData.ClusterPermissionsMissing -f $sqlServer, $sqlInstanceName ) - } - - return $clusterPermissionsPresent -} - -<# - .SYNOPSIS - Determine if the current node is hosting the instance. - - .PARAMETER ServerObject - The server object on which to perform the test. -#> -function Test-ActiveNode -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter(Mandatory = $true)] - [Microsoft.SqlServer.Management.Smo.Server] - $ServerObject - ) - - $result = $false - - # Determine if this is a failover cluster instance (FCI) - if ( $ServerObject.IsMemberOfWsfcCluster ) - { - <# - If the current node name is the same as the name the instances is - running on, then this is the active node - #> - $result = $ServerObject.ComputerNamePhysicalNetBIOS -eq (Get-ComputerName) - } - else - { - <# - This is a standalone instance, therefore the node will always host - the instance. - #> - $result = $true - } - - return $result -} - -<# - .SYNOPSIS - Execute an SQL script located in a file on disk. - - .PARAMETER ServerInstance - The name of an instance of the Database Engine. - For default instances, only specify the computer name. For named instances, - use the format ComputerName\InstanceName. - - .PARAMETER InputFile - Path to SQL script file that will be executed. - - .PARAMETER Query - The full query that will be executed. - - .PARAMETER Credential - The credentials to use to authenticate using SQL Authentication. To - authenticate using Windows Authentication, assign the credentials - to the built-in parameter 'PsDscRunAsCredential'. If both parameters - 'Credential' and 'PsDscRunAsCredential' are not assigned, then the - SYSTEM account will be used to authenticate using Windows Authentication. - - .PARAMETER QueryTimeout - Specifies, as an integer, the number of seconds after which the T-SQL - script execution will time out. In some SQL Server versions there is a - bug in Invoke-SqlCmd where the normal default value 0 (no timeout) is not - respected and the default value is incorrectly set to 30 seconds. - - .PARAMETER Variable - Creates a Invoke-SqlCmd scripting variable for use in the Invoke-SqlCmd - script, and sets a value for the variable. - - .PARAMETER DisableVariables - Specifies, as a boolean, whether or not PowerShell will ignore Invoke-SqlCmd - scripting variables that share a format such as $(variable_name). For more - information how to use this, please go to the help documentation for - [Invoke-SqlCmd](https://docs.microsoft.com/en-us/powershell/module/sqlserver/Invoke-Sqlcmd). - - .PARAMETER Encrypt - Specifies how encryption should be enforced. When not specified, the default - value is `Mandatory`. - - This value maps to the Encrypt property SqlConnectionEncryptOption - on the SqlConnection object of the Microsoft.Data.SqlClient driver. - - This parameter can only be used when the module SqlServer v22.x.x is installed. - - .NOTES - This wrapper for Invoke-SqlCmd make verbose functionality of PRINT and - RAISEERROR statements work as those are outputted in the verbose output - stream. For some reason having the wrapper in a separate module seems to - trigger (so that it works getting) the verbose output for those statements. - - Parameter `Encrypt` controls whether the connection used by `Invoke-SqlCmd` - should enforce encryption. This parameter can only be used together with the - module _SqlServer_ v22.x (minimum v22.0.49-preview). The parameter will be - ignored if an older major versions of the module _SqlServer_ is used. - Encryption is mandatory by default, which generates the following exception - when the correct certificates are not present: - - "A connection was successfully established with the server, but then - an error occurred during the login process. (provider: SSL Provider, - error: 0 - The certificate chain was issued by an authority that is - not trusted.)" - - For more details, see the article [Connect to SQL Server with strict encryption](https://learn.microsoft.com/en-us/sql/relational-databases/security/networking/connect-with-strict-encryption?view=sql-server-ver16) - and [Configure SQL Server Database Engine for encrypting connections](https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/configure-sql-server-encryption?view=sql-server-ver16). -#> -function Invoke-SqlScript -{ - [CmdletBinding()] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $ServerInstance, - - [Parameter(ParameterSetName = 'File', Mandatory = $true)] - [System.String] - $InputFile, - - [Parameter(ParameterSetName = 'Query', Mandatory = $true)] - [System.String] - $Query, - - [Parameter()] - [System.Management.Automation.PSCredential] - [System.Management.Automation.Credential()] - $Credential, - - [Parameter()] - [System.UInt32] - $QueryTimeout, - - [Parameter()] - [System.String[]] - $Variable, - - [Parameter()] - [System.Boolean] - $DisableVariables, - - [Parameter()] - [ValidateSet('Mandatory', 'Optional', 'Strict')] - [System.String] - $Encrypt - ) - - Import-SqlDscPreferredModule - - if ($PSCmdlet.ParameterSetName -eq 'File') - { - $null = $PSBoundParameters.Remove('Query') - } - elseif ($PSCmdlet.ParameterSetName -eq 'Query') - { - $null = $PSBoundParameters.Remove('InputFile') - } - - if ($null -ne $Credential) - { - $null = $PSBoundParameters.Add('Username', $Credential.UserName) - - $null = $PSBoundParameters.Add('Password', $Credential.GetNetworkCredential().Password) - } - - $null = $PSBoundParameters.Remove('Credential') - - if ($PSBoundParameters.ContainsKey('Encrypt')) - { - $commandInvokeSqlCmd = Get-Command -Name 'Invoke-SqlCmd' - - if ($null -ne $commandInvokeSqlCmd -and $commandInvokeSqlCmd.Parameters.Keys -notcontains 'Encrypt') - { - $null = $PSBoundParameters.Remove('Encrypt') - } - } - - if ([System.String]::IsNullOrEmpty($Variable)) - { - $null = $PSBoundParameters.Remove('Variable') - } - - Invoke-SqlCmd @PSBoundParameters -} - -<# - .SYNOPSIS - Builds service account parameters for service account. - - .PARAMETER ServiceAccount - Credential for the service account. -#> -function Get-ServiceAccount -{ - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - [Parameter(Mandatory = $true)] - [System.Management.Automation.PSCredential] - $ServiceAccount - ) - - $accountParameters = @{ } - - switch -Regex ($ServiceAccount.UserName.ToUpper()) - { - '^(?:NT ?AUTHORITY\\)?(SYSTEM|LOCALSERVICE|LOCAL SERVICE|NETWORKSERVICE|NETWORK SERVICE)$' - { - $accountParameters = @{ - UserName = "NT AUTHORITY\$($Matches[1])" - } - } - - '^(?:NT SERVICE\\)(.*)$' - { - $accountParameters = @{ - UserName = "NT SERVICE\$($Matches[1])" - } - } - - # Testing if account is a Managed Service Account, which ends with '$'. - '\$$' - { - $accountParameters = @{ - UserName = $ServiceAccount.UserName - } - } - - # Normal local or domain service account. - default - { - $accountParameters = @{ - UserName = $ServiceAccount.UserName - Password = $ServiceAccount.GetNetworkCredential().Password - } - } - } - - return $accountParameters -} - -<# - .SYNOPSIS - Recursively searches Exception stack for specific error number. - - .PARAMETER ExceptionToSearch - The Exception object to test - - .PARAMETER ErrorNumber - The specific error number to look for - - .NOTES - This function allows us to more easily write mocks. -#> -function Find-ExceptionByNumber -{ - # Define parameters - param - ( - [Parameter(Mandatory = $true)] - [System.Exception] - $ExceptionToSearch, - - [Parameter(Mandatory = $true)] - [System.String] - $ErrorNumber - ) - - # Define working variables - $errorFound = $false - - # Check to see if the exception has an inner exception - if ($ExceptionToSearch.InnerException) - { - # Assign found to the returned recursive call - $errorFound = Find-ExceptionByNumber -ExceptionToSearch $ExceptionToSearch.InnerException -ErrorNumber $ErrorNumber - } - - # Check to see if it was found - if (!$errorFound) - { - # Check this exceptions message - $errorFound = $ExceptionToSearch.Number -eq $ErrorNumber - } - - # Return - return $errorFound -} - -<# - .SYNOPSIS - Converts the combination of server name and instance name to - the correct server instance name. - - .PARAMETER InstanceName - Specifies the name of the SQL Server instance on the host. - - .PARAMETER ServerName - Specifies the host name of the SQL Server. -#> -function ConvertTo-ServerInstanceName -{ - [CmdletBinding()] - [OutputType([System.String])] - param - ( - [Parameter(Mandatory = $true)] - [System.String] - $InstanceName, - - [Parameter(Mandatory = $true)] - [System.String] - $ServerName - ) - - if ($InstanceName -eq 'MSSQLSERVER') - { - $serverInstance = $ServerName - } - else - { - $serverInstance = '{0}\{1}' -f $ServerName, $InstanceName - } - - return $serverInstance -} - -<# - .SYNOPSIS - Test if the specific feature flag should be enabled. - - .PARAMETER FeatureFlag - An array of feature flags that should be compared against. - - .PARAMETER TestFlag - The feature flag that is being check if it should be enabled. -#> -function Test-FeatureFlag -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter()] - [System.String[]] - $FeatureFlag, - - [Parameter(Mandatory = $true)] - [System.String] - $TestFlag - ) - - $flagEnabled = $FeatureFlag -and ($FeatureFlag -and $FeatureFlag.Contains($TestFlag)) - - return $flagEnabled -} diff --git a/source/Modules/SqlServerDsc.Common/prefix.ps1 b/source/Modules/SqlServerDsc.Common/prefix.ps1 new file mode 100644 index 000000000..a98f01048 --- /dev/null +++ b/source/Modules/SqlServerDsc.Common/prefix.ps1 @@ -0,0 +1,5 @@ +$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' + +Import-Module -Name $script:resourceHelperModulePath + +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' diff --git a/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 b/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 index 6cf7d549d..0eb8ccf9b 100644 --- a/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 +++ b/tests/Unit/Public/Invoke-SqlDscQuery.Tests.ps1 @@ -62,7 +62,9 @@ Describe 'Invoke-SqlDscQuery' -Tag 'Public' { MockExpectedParameters = '-ServerObject -DatabaseName -Query [-PassThru] [-StatementTimeout ] [-RedactText ] [-Force] [-WhatIf] [-Confirm] []' } ) { - $result = (Get-Command -Name 'Invoke-SqlDscQuery').ParameterSets | + $result = (Get-Command -Name 'Invoke-SqlDscQuery').ParameterSets + + $result = $result | Where-Object -FilterScript { $_.Name -eq $mockParameterSetName } | diff --git a/tests/Unit/SqlServerDsc.Common.Tests.ps1 b/tests/Unit/SqlServerDsc.Common.Tests.ps1 deleted file mode 100644 index 9ab45e795..000000000 --- a/tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ /dev/null @@ -1,3218 +0,0 @@ -<# - .SYNOPSIS - Unit test for helper functions in module SqlServerDsc.Common. - - .NOTES - SMO stubs - --------- - These are loaded at the start so that it is known that they are left in the - session after test finishes, and will spill over to other tests. There does - not exist a way to unload assemblies. It is possible to load these in a - InModuleScope but the classes are still present in the parent scope when - Pester has ran. - - SqlServer/SQLPS stubs - --------------------- - These are imported using Import-SqlModuleStub in a BeforeAll-block in only - a test that requires them, and must be removed in an AfterAll-block using - Remove-SqlModuleStub so the stub cmdlets does not spill over to another - test. -#> - -# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] -# Suppressing this rule because Script Analyzer does not understand Pester's syntax. -[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] -param () - -BeforeDiscovery { - try - { - if (-not (Get-Module -Name 'DscResource.Test')) - { - # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null - } - - # If the dependencies have 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 noop" first.' - } -} - -BeforeAll { - $script:moduleName = 'SqlServerDsc' - $script:subModuleName = 'SqlServerDsc.Common' - - $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 - $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' - - $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName - - Import-Module -Name $script:subModulePath -ErrorAction 'Stop' - - Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') - - # Loading SMO stubs. - if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) - { - Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath 'Stubs') -ChildPath 'SMO.cs') - } - - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName - $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName -} - -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:subModuleName -All | Remove-Module -Force - - # Remove module common test helper. - Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force -} - -# Tests only the parts of the code that does not already get tested thru the other tests. -Describe 'SqlServerDsc.Common\Copy-ItemWithRobocopy' -Tag 'CopyItemWithRobocopy' { - BeforeAll { - $mockRobocopyExecutableName = 'Robocopy.exe' - $mockRobocopyExecutableVersionWithoutUnbufferedIO = '6.2.9200.00000' - $mockRobocopyExecutableVersionWithUnbufferedIO = '6.3.9600.16384' - $mockRobocopyExecutableVersion = '' # Set dynamically during runtime - $mockRobocopyArgumentSilent = '/njh /njs /ndl /nc /ns /nfl' - $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty = '/e' - $mockRobocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource = '/purge' - $mockRobocopyArgumentUseUnbufferedIO = '/J' - $mockRobocopyArgumentSourcePath = 'C:\Source\SQL2016' - $mockRobocopyArgumentDestinationPath = 'D:\Temp' - $mockRobocopyArgumentSourcePathWithSpaces = 'C:\Source\SQL2016 STD SP1' - $mockRobocopyArgumentDestinationPathWithSpaces = 'D:\Temp\DSC SQL2016' - - $mockGetCommand = { - return @( - ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'Name' -Value $mockRobocopyExecutableName -PassThru | - Add-Member -MemberType ScriptProperty -Name FileVersionInfo -Value { - return @( ( New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'ProductVersion' -Value $mockRobocopyExecutableVersion -PassThru -Force - ) ) - } -PassThru -Force - ) - ) - } - - $mockStartSqlSetupProcessExpectedArgument = '' # Set dynamically during runtime - $mockStartSqlSetupProcessExitCode = 0 # Set dynamically during runtime - - $mockStartSqlSetupProcess_Robocopy = { - if ( $ArgumentList -cne $mockStartSqlSetupProcessExpectedArgument ) - { - throw "Expected arguments was not the same as the arguments in the function call.`nExpected: '$mockStartSqlSetupProcessExpectedArgument' `n But was: '$ArgumentList'" - } - - return New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value 0 -PassThru -Force - } - - $mockStartSqlSetupProcess_Robocopy_WithExitCode = { - return New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value $mockStartSqlSetupProcessExitCode -PassThru -Force - } - } - - Context 'When Copy-ItemWithRobocopy is called it should return the correct arguments' { - BeforeEach { - Mock -CommandName Get-Command -MockWith $mockGetCommand - Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy - $mockRobocopyArgumentSourcePathQuoted = '"{0}"' -f $mockRobocopyArgumentSourcePath - $mockRobocopyArgumentDestinationPathQuoted = '"{0}"' -f $mockRobocopyArgumentDestinationPath - } - - - It 'Should use Unbuffered IO when copying' { - $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO - - $mockStartSqlSetupProcessExpectedArgument = - $mockRobocopyArgumentSourcePathQuoted, - $mockRobocopyArgumentDestinationPathQuoted, - $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty, - $mockRobocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource, - $mockRobocopyArgumentUseUnbufferedIO, - $mockRobocopyArgumentSilent -join ' ' - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - - It 'Should not use Unbuffered IO when copying' { - $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithoutUnbufferedIO - - $mockStartSqlSetupProcessExpectedArgument = - $mockRobocopyArgumentSourcePathQuoted, - $mockRobocopyArgumentDestinationPathQuoted, - $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty, - $mockRobocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource, - '', - $mockRobocopyArgumentSilent -join ' ' - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - } - - Context 'When Copy-ItemWithRobocopy throws an exception it should return the correct error messages' { - BeforeAll { - $mockRobocopyArgumentSourcePath = 'C:\Source\SQL2016' - $mockRobocopyArgumentDestinationPath = 'D:\Temp\DSCSQL2016' - $mockRobocopyExecutableName = 'Robocopy.exe' - $mockRobocopyExecutableVersion = '' # Set dynamically during runtime - } - - BeforeEach { - $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO - - Mock -CommandName Get-Command -MockWith { - return @( - ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'Name' -Value $mockRobocopyExecutableName -PassThru | - Add-Member -MemberType ScriptProperty -Name FileVersionInfo -Value { - return @( ( New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'ProductVersion' -Value $mockRobocopyExecutableVersion -PassThru -Force - ) ) - } -PassThru -Force - ) - ) - } - - Mock -CommandName Start-Process -MockWith { - return New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value $mockStartSqlSetupProcessExitCode -PassThru -Force - } - } - - It 'Should throw the correct error message when error code is 8' { - $mockStartSqlSetupProcessExitCode = 8 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.RobocopyErrorCopying - } - - $mockErrorMessage = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f $mockStartSqlSetupProcessExitCode - ) - - $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty - - { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should -Throw -ExpectedMessage $mockErrorMessage - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - - It 'Should throw the correct error message when error code is 16' { - $mockStartSqlSetupProcessExitCode = 16 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.RobocopyErrorCopying - } - - $mockErrorMessage = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f $mockStartSqlSetupProcessExitCode - ) - - $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty - - { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should -Throw -ExpectedMessage $mockErrorMessage - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -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)' { - $mockStartSqlSetupProcessExitCode = 9 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.RobocopyFailuresCopying - } - - $mockErrorMessage = Get-InvalidResultRecord -Message ( - $mockLocalizedString -f $mockStartSqlSetupProcessExitCode - ) - - $mockErrorMessage | Should -Not -BeNullOrEmpty - - { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should -Throw -ExpectedMessage $mockErrorMessage - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - } - - Context 'When Copy-ItemWithRobocopy is called and finishes successfully it should return the correct exit code' { - BeforeEach { - $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO - - Mock -CommandName Get-Command -MockWith $mockGetCommand - Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy_WithExitCode - } - - AfterEach { - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - - It 'Should finish successfully with exit code 1' { - $mockStartSqlSetupProcessExitCode = 1 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - - It 'Should finish successfully with exit code 2' { - $mockStartSqlSetupProcessExitCode = 2 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - - It 'Should finish successfully with exit code 3' { - $mockStartSqlSetupProcessExitCode = 3 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePath - DestinationPath = $mockRobocopyArgumentDestinationPath - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - } - } - - Context 'When Copy-ItemWithRobocopy is called with spaces in paths and finishes successfully it should return the correct exit code' { - BeforeEach { - $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO - - Mock -CommandName Get-Command -MockWith $mockGetCommand - Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy_WithExitCode - } - - AfterEach { - Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It - } - - It 'Should finish successfully with exit code 1' { - $mockStartSqlSetupProcessExitCode = 1 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePathWithSpaces - DestinationPath = $mockRobocopyArgumentDestinationPathWithSpaces - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - } - - It 'Should finish successfully with exit code 2' { - $mockStartSqlSetupProcessExitCode = 2 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePathWithSpaces - DestinationPath = $mockRobocopyArgumentDestinationPathWithSpaces - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - } - - It 'Should finish successfully with exit code 3' { - $mockStartSqlSetupProcessExitCode = 3 - - $copyItemWithRobocopyParameter = @{ - Path = $mockRobocopyArgumentSourcePathWithSpaces - DestinationPath = $mockRobocopyArgumentDestinationPathWithSpaces - } - - $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' - } - } -} - -Describe 'SqlServerDsc.Common\Invoke-InstallationMediaCopy' -Tag 'InvokeInstallationMediaCopy' { - BeforeAll { - $mockSourcePathGuid = 'cc719562-0f46-4a16-8605-9f8a47c70402' - $mockDestinationPath = 'C:\Users\user\AppData\Local\Temp' - - $mockShareCredentialUserName = 'COMPANY\SqlAdmin' - $mockShareCredentialPassword = 'dummyPassW0rd' - $mockShareCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( - $mockShareCredentialUserName, - ($mockShareCredentialPassword | ConvertTo-SecureString -AsPlainText -Force) - ) - - $mockGetTemporaryFolder = { - return $mockDestinationPath - } - - $mockNewGuid = { - return New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'Guid' -Value $mockSourcePathGuid -PassThru -Force - } - - Mock -CommandName Connect-UncPath - Mock -CommandName Disconnect-UncPath - Mock -CommandName Copy-ItemWithRobocopy - Mock -CommandName Get-TemporaryFolder -MockWith $mockGetTemporaryFolder - Mock -CommandName New-Guid -MockWith $mockNewGuid - } - - Context 'When invoking installation media copy, using SourcePath containing leaf' { - BeforeAll { - $mockSourcePathUNCWithLeaf = '\\server\share\leaf' - - Mock -CommandName Join-Path -MockWith { - return $mockDestinationPath + '\leaf' - } - } - - It 'Should call the correct mocks' { - $invokeInstallationMediaCopyParameters = @{ - SourcePath = $mockSourcePathUNCWithLeaf - SourceCredential = $mockShareCredential - } - - $null = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It - Should -Invoke -CommandName New-Guid -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Get-TemporaryFolder -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Copy-ItemWithRobocopy -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It - } - - It 'Should return the correct destination path' { - $invokeInstallationMediaCopyParameters = @{ - SourcePath = $mockSourcePathUNCWithLeaf - SourceCredential = $mockShareCredential - PassThru = $true - } - - $invokeInstallationMediaCopyResult = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters - - $invokeInstallationMediaCopyResult | Should -Be ('{0}\leaf' -f $mockDestinationPath) - } - } - - Context 'When invoking installation media copy, using SourcePath containing a second leaf' { - BeforeAll { - $mockSourcePathUNCWithLeaf = '\\server\share\leaf\secondleaf' - - Mock -CommandName Join-Path -MockWith { - return $mockDestinationPath + '\secondleaf' - } - } - - It 'Should call the correct mocks' { - $invokeInstallationMediaCopyParameters = @{ - SourcePath = $mockSourcePathUNCWithLeaf - SourceCredential = $mockShareCredential - } - - $null = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It - Should -Invoke -CommandName New-Guid -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Get-TemporaryFolder -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Copy-ItemWithRobocopy -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It - } - - It 'Should return the correct destination path' { - $invokeInstallationMediaCopyParameters = @{ - SourcePath = $mockSourcePathUNCWithLeaf - SourceCredential = $mockShareCredential - PassThru = $true - } - - $invokeInstallationMediaCopyResult = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters - - $invokeInstallationMediaCopyResult | Should -Be ('{0}\secondleaf' -f $mockDestinationPath) - } - } - - Context 'When invoking installation media copy, using SourcePath without a leaf' { - BeforeAll { - $mockSourcePathUNC = '\\server\share' - - Mock -CommandName Join-Path -MockWith { - return $mockDestinationPath + '\' + $mockSourcePathGuid - } - } - - It 'Should call the correct mocks' { - $invokeInstallationMediaCopyParameters = @{ - SourcePath = $mockSourcePathUNC - SourceCredential = $mockShareCredential - } - - $null = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It - Should -Invoke -CommandName New-Guid -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Get-TemporaryFolder -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Copy-ItemWithRobocopy -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It - } - - It 'Should return the correct destination path' { - $invokeInstallationMediaCopyParameters = @{ - SourcePath = $mockSourcePathUNC - SourceCredential = $mockShareCredential - PassThru = $true - } - - $invokeInstallationMediaCopyResult = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters - $invokeInstallationMediaCopyResult | Should -Be ('{0}\{1}' -f $mockDestinationPath, $mockSourcePathGuid) - } - } -} - -Describe 'SqlServerDsc.Common\Connect-UncPath' -Tag 'ConnectUncPath' { - BeforeAll { - $mockSourcePathUNC = '\\server\share' - - $mockShareCredentialUserName = 'COMPANY\SqlAdmin' - $mockShareCredentialPassword = 'dummyPassW0rd' - $mockShareCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( - $mockShareCredentialUserName, - ($mockShareCredentialPassword | ConvertTo-SecureString -AsPlainText -Force) - ) - - $mockFqdnShareCredentialUserName = 'SqlAdmin@company.local' - $mockFqdnShareCredentialPassword = 'dummyPassW0rd' - $mockFqdnShareCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( - $mockFqdnShareCredentialUserName, - ($mockFqdnShareCredentialPassword | ConvertTo-SecureString -AsPlainText -Force) - ) - - InModuleScope -ScriptBlock { - # Stubs for cross-platform testing. - function script:New-SmbMapping - { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Suppressing this rule because parameter Password is used to mock the real command.')] - [CmdletBinding()] - param - ( - [Parameter()] - [System.String] - $RemotePath, - - [Parameter()] - [System.String] - $UserName, - - [Parameter()] - [System.String] - $Password - ) - - throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand - } - - function script:Remove-SmbMapping - { - throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand - } - } - - Mock -CommandName New-SmbMapping -MockWith { - return @{ - RemotePath = $mockSourcePathUNC - } - } - } - - AfterAll { - InModuleScope -ScriptBlock { - Remove-Item -Path 'function:/New-SmbMapping' - Remove-Item -Path 'function:/Remove-SmbMapping' - } - } - - Context 'When connecting to a UNC path without credentials (using current credentials)' { - It 'Should call the correct mocks' { - $connectUncPathParameters = @{ - RemotePath = $mockSourcePathUNC - } - - $null = Connect-UncPath @connectUncPathParameters -ErrorAction 'Stop' - - Should -Invoke -CommandName New-SmbMapping -ParameterFilter { - <# - Due to issue https://github.com/pester/Pester/issues/1542 - we must use `$null -ne $UserName` instead of - `$PSBoundParameters.ContainsKey('UserName') -eq $false`. - #> - $RemotePath -eq $mockSourcePathUNC ` - -and $null -eq $UserName - } -Exactly -Times 1 -Scope It - } - } - - Context 'When connecting to a UNC path with specific credentials' { - It 'Should call the correct mocks' { - $connectUncPathParameters = @{ - RemotePath = $mockSourcePathUNC - SourceCredential = $mockShareCredential - } - - $null = Connect-UncPath @connectUncPathParameters -ErrorAction 'Stop' - - Should -Invoke -CommandName New-SmbMapping -ParameterFilter { - $RemotePath -eq $mockSourcePathUNC ` - -and $UserName -eq $mockShareCredentialUserName - } -Exactly -Times 1 -Scope It - } - } - - Context 'When connecting using Fully Qualified Domain Name (FQDN)' { - It 'Should call the correct mocks' { - $connectUncPathParameters = @{ - RemotePath = $mockSourcePathUNC - SourceCredential = $mockFqdnShareCredential - } - - $null = Connect-UncPath @connectUncPathParameters -ErrorAction 'Stop' - - Should -Invoke -CommandName New-SmbMapping -ParameterFilter { - $RemotePath -eq $mockSourcePathUNC ` - -and $UserName -eq $mockFqdnShareCredentialUserName - } -Exactly -Times 1 -Scope It - } - } - - Context 'When connecting to a UNC path and using parameter PassThru' { - It 'Should return the correct MSFT_SmbMapping object' { - $connectUncPathParameters = @{ - RemotePath = $mockSourcePathUNC - SourceCredential = $mockShareCredential - PassThru = $true - } - - $connectUncPathResult = Connect-UncPath @connectUncPathParameters - $connectUncPathResult.RemotePath | Should -Be $mockSourcePathUNC - } - } -} - -Describe 'SqlServerDsc.Common\Disconnect-UncPath' -Tag 'DisconnectUncPath' { - BeforeAll { - $mockSourcePathUNC = '\\server\share' - - InModuleScope -ScriptBlock { - # Stubs for cross-platform testing. - function script:Remove-SmbMapping - { - throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand - } - } - - Mock -CommandName Remove-SmbMapping - } - - AfterAll { - InModuleScope -ScriptBlock { - Remove-Item -Path 'function:/Remove-SmbMapping' - } - } - - Context 'When disconnecting from an UNC path' { - It 'Should call the correct mocks' { - $disconnectUncPathParameters = @{ - RemotePath = $mockSourcePathUNC - } - - $null = Disconnect-UncPath @disconnectUncPathParameters - - Should -Invoke -CommandName Remove-SmbMapping -Exactly -Times 1 -Scope It - } - } -} - -Describe 'SqlServerDsc.Common\Start-SqlSetupProcess' -Tag 'StartSqlSetupProcess' { - BeforeAll { - $mockPowerShellExecutable = if ($IsLinux -or $IsMacOS) - { - 'pwsh' - } - else - { - 'powershell.exe' - } - } - Context 'When starting a process successfully' { - It 'Should return exit code 0' { - $startSqlSetupProcessParameters = @{ - FilePath = $mockPowerShellExecutable - ArgumentList = '-NonInteractive -NoProfile -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 = $mockPowerShellExecutable - ArgumentList = '-NonInteractive -NoProfile -Command &{Start-Sleep -Seconds 4}' - Timeout = 2 - } - - { Start-SqlSetupProcess @startSqlSetupProcessParameters } | Should -Throw -ErrorId 'ProcessNotTerminated,Microsoft.PowerShell.Commands.WaitProcessCommand' - } - } -} - -Describe 'SqlServerDsc.Common\Restart-SqlService' -Tag 'RestartSqlService' { - BeforeAll { - InModuleScope -ScriptBlock { - # Stubs for cross-platform testing. - function script:Get-Service - { - throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand - } - - function script:Restart-Service - { - throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand - } - - function script:Start-Service - { - throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand - } - } - } - - AfterAll { - InModuleScope -ScriptBlock { - # Remove stubs that was used for cross-platform testing. - Remove-Item -Path function:Get-Service - Remove-Item -Path function:Restart-Service - Remove-Item -Path function:Start-Service - } - } - - Context 'Restart-SqlService standalone instance' { - Context 'When the Windows services should be restarted' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return @{ - Name = 'MSSQLSERVER' - ServiceName = 'MSSQLSERVER' - Status = 'Online' - IsClustered = $false - } - } - - Mock -CommandName Get-Service -MockWith { - return @{ - Name = 'MSSQLSERVER' - DisplayName = 'Microsoft SQL Server (MSSQLSERVER)' - DependentServices = @( - @{ - Name = 'SQLSERVERAGENT' - DisplayName = 'SQL Server Agent (MSSQLSERVER)' - Status = 'Running' - DependentServices = @() - } - ) - } - } - - Mock -CommandName Restart-Service - Mock -CommandName Start-Service - Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName - } - - It 'Should restart SQL Service and running SQL Agent service' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert just the first call to Connect-SQL. - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. - #> - $ErrorAction -ne 'SilentlyContinue' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 0 -ModuleName $subModuleName - Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 1 - } - - Context 'When skipping the cluster check' { - It 'Should restart SQL Service and running SQL Agent service' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -SkipClusterCheck -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert just the first call to Connect-SQL. - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. - #> - $ErrorAction -ne 'SilentlyContinue' - } -Scope It -Exactly -Times 0 - - Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 0 -ModuleName $subModuleName - Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 1 - } - } - - Context 'When skipping the online check' { - It 'Should restart SQL Service and running SQL Agent service and not wait for the SQL Server instance to come back online' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -SkipWaitForOnline -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert just the first call to Connect-SQL. - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. - #> - $ErrorAction -ne 'SilentlyContinue' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert the second call to Connect-SQL - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $true`. - #> - $ErrorAction -eq 'SilentlyContinue' - } -Scope It -Exactly -Times 0 - - Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 0 -ModuleName $subModuleName - Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 1 - } - } - } - - Context 'When the SQL Server instance is a Failover Cluster instance' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return @{ - Name = 'MSSQLSERVER' - ServiceName = 'MSSQLSERVER' - Status = 'Online' - IsClustered = $true - } - } - - Mock -CommandName Get-Service - Mock -CommandName Restart-Service - Mock -CommandName Start-Service - Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName - } - - It 'Should just call Restart-SqlClusterService to restart the SQL Server cluster instance' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' - - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert just the first call to Connect-SQL. - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. - #> - $ErrorAction -ne 'SilentlyContinue' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 1 -ModuleName $subModuleName - Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 0 - Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 0 - Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 0 - } - - Context 'When passing the Timeout value' { - It 'Should just call Restart-SqlClusterService with the correct parameter' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -Timeout 120 -ErrorAction 'Stop' - - Should -Invoke -CommandName Restart-SqlClusterService -ParameterFilter { - <# - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('Timeout') -eq $true`. - #> - $null -ne $Timeout - } -Scope It -Exactly -Times 1 -ModuleName $subModuleName - } - } - - Context 'When passing the OwnerNode value' { - It 'Should just call Restart-SqlClusterService with the correct parameter' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -OwnerNode @('TestNode') -ErrorAction 'Stop' - - Should -Invoke -CommandName Restart-SqlClusterService -ParameterFilter { - <# - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('OwnerNode') -eq $true`. - #> - $null -ne $OwnerNode - } -Scope It -Exactly -Times 1 -ModuleName $subModuleName - } - } - } - - Context 'When the Windows services should be restarted but there is not SQL Agent service' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return @{ - Name = 'NOAGENT' - InstanceName = 'NOAGENT' - ServiceName = 'NOAGENT' - Status = 'Online' - } - } - - Mock -CommandName Get-Service -MockWith { - return @{ - Name = 'MSSQL$NOAGENT' - DisplayName = 'Microsoft SQL Server (NOAGENT)' - DependentServices = @() - } - } - - Mock -CommandName Restart-Service - Mock -CommandName Start-Service - Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName - } - - It 'Should restart SQL Service and not try to restart missing SQL Agent service' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'NOAGENT' -SkipClusterCheck -ErrorAction 'Stop' - - Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 0 - } - } - - Context 'When the Windows services should be restarted but the SQL Agent service is stopped' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return @{ - Name = 'STOPPEDAGENT' - InstanceName = 'STOPPEDAGENT' - ServiceName = 'STOPPEDAGENT' - Status = 'Online' - } - } - - Mock -CommandName Get-Service -MockWith { - return @{ - Name = 'MSSQL$STOPPEDAGENT' - DisplayName = 'Microsoft SQL Server (STOPPEDAGENT)' - DependentServices = @( - @{ - Name = 'SQLAGENT$STOPPEDAGENT' - DisplayName = 'SQL Server Agent (STOPPEDAGENT)' - Status = 'Stopped' - DependentServices = @() - } - ) - } - } - - Mock -CommandName Restart-Service - Mock -CommandName Start-Service - Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName - } - - It 'Should restart SQL Service and not try to restart stopped SQL Agent service' { - $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'STOPPEDAGENT' -SkipClusterCheck -ErrorAction 'Stop' - - Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 0 - } - } - - Context 'When it fails to connect to the instance within the timeout period' { - Context 'When the connection throws an exception' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - # Using SilentlyContinue to not show the errors in the Pester output. - Write-Error -Message 'Mock connection error' -ErrorAction 'SilentlyContinue' - } - - Mock -CommandName Get-Service -MockWith { - return @{ - Name = 'MSSQLSERVER' - DisplayName = 'Microsoft SQL Server (MSSQLSERVER)' - DependentServices = @( - @{ - Name = 'SQLSERVERAGENT' - DisplayName = 'SQL Server Agent (MSSQLSERVER)' - Status = 'Running' - DependentServices = @() - } - ) - } - } - - Mock -CommandName Restart-Service - Mock -CommandName Start-Service - } - - It 'Should wait for timeout before throwing error message' { - $mockLocalizedString = InModuleScope -ScriptBlock { - $localizedData.FailedToConnectToInstanceTimeout - } - - $mockErrorMessage = Get-InvalidOperationRecord -Message ( - ($mockLocalizedString -f (Get-ComputerName), 'MSSQLSERVER', 4) + '*Mock connection error*' - ) - - $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty - - { - Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -Timeout 4 -SkipClusterCheck - } | Should -Throw -ExpectedMessage $mockErrorMessage - - <# - Not using -Exactly to handle when CI is slower, result is - that there are 3 calls to Connect-SQL. - #> - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert the second call to Connect-SQL - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $true`. - #> - $ErrorAction -eq 'SilentlyContinue' - } -Scope It -Times 2 - } - } - - Context 'When the Status returns offline' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return @{ - Name = 'MSSQLSERVER' - InstanceName = '' - ServiceName = 'MSSQLSERVER' - Status = 'Offline' - } - } - - Mock -CommandName Get-Service -MockWith { - return @{ - Name = 'MSSQLSERVER' - DisplayName = 'Microsoft SQL Server (MSSQLSERVER)' - DependentServices = @( - @{ - Name = 'SQLSERVERAGENT' - DisplayName = 'SQL Server Agent (MSSQLSERVER)' - Status = 'Running' - DependentServices = @() - } - ) - } - } - - Mock -CommandName Restart-Service - Mock -CommandName Start-Service - } - - It 'Should wait for timeout before throwing error message' { - $mockLocalizedString = InModuleScope -ScriptBlock { - $localizedData.FailedToConnectToInstanceTimeout - } - - $mockErrorMessage = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f (Get-ComputerName), 'MSSQLSERVER', 4 - ) - - $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty - - { - Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -Timeout 4 -SkipClusterCheck - } | Should -Throw -ExpectedMessage $mockErrorMessage - - <# - Not using -Exactly to handle when CI is slower, result is - that there are 3 calls to Connect-SQL. - #> - Should -Invoke -CommandName Connect-SQL -ParameterFilter { - <# - Make sure we assert the second call to Connect-SQL - - Due to issue https://github.com/pester/Pester/issues/1542 - we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $true`. - #> - $ErrorAction -eq 'SilentlyContinue' - } -Scope It -Times 2 - } - } - } - } -} - -# This test is skipped on Linux and macOS due to it is missing CIM Instance. -Describe 'SqlServerDsc.Common\Restart-SqlClusterService' -Tag 'RestartSqlClusterService' -Skip:($IsLinux -or $IsMacOS) { - Context 'When not clustered instance is found' { - BeforeAll { - Mock -CommandName Get-CimInstance - Mock -CommandName Get-CimAssociatedInstance - Mock -CommandName Invoke-CimMethod - } - - It 'Should not restart any cluster resources' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - } - } - - Context 'When clustered instance is offline' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 3 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance - Mock -CommandName Invoke-CimMethod - } - - It 'Should not restart any cluster resources' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 0 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' - } -Scope It -Exactly -Times 0 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' - } -Scope It -Exactly -Times 0 - } - } - - Context 'When restarting a Sql Server clustered instance' { - Context 'When it is the default instance' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Invoke-CimMethod - } - - It 'Should restart SQL Server cluster resource and the SQL Agent cluster resource' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - } - } - - Context 'When it is a named instance' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (DSCTEST)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'DSCTEST' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Invoke-CimMethod - } - - It 'Should restart SQL Server cluster resource and the SQL Agent cluster resource' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'DSCTEST' -ErrorAction 'Stop' - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (DSCTEST)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (DSCTEST)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (DSCTEST)' - } -Scope It -Exactly -Times 1 - } - } - } - - Context 'When restarting a Sql Server clustered instance and the SQL Agent is offline' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' - # Mock the resource to be offline. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 3 -TypeName 'Int32' - - return $mock - } - - Mock -CommandName Invoke-CimMethod - } - - It 'Should restart the SQL Server cluster resource and ignore the SQL Agent cluster resource online ' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - } - } - - Context 'When passing the parameter OwnerNode' { - Context 'When both the SQL Server and SQL Agent cluster resources is owned by the current node' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' - # Mock the resource to be offline. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' - - return $mock - } - - Mock -CommandName Invoke-CimMethod - } - - It 'Should restart the SQL Server cluster resource and the SQL Agent cluster resource' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - } - } - - Context 'When both the SQL Server and SQL Agent cluster resources is owned by the current node but the SQL Agent cluster resource is offline' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' - # Mock the resource to be offline. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 3 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' - - return $mock - } - - Mock -CommandName Invoke-CimMethod - } - - It 'Should only restart the SQL Server cluster resource' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' - } -Scope It -Exactly -Times 0 - } - } - - Context 'When only the SQL Server cluster resources is owned by the current node' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' - # Mock the resource to be offline. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE2' -TypeName 'String' - - return $mock - } - - Mock -CommandName Invoke-CimMethod - } - - It 'Should only restart the SQL Server cluster resource' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' - } -Scope It -Exactly -Times 1 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' - } -Scope It -Exactly -Times 0 - } - } - - Context 'When the SQL Server cluster resources is not owned by the current node' { - BeforeAll { - Mock -CommandName Get-CimInstance -MockWith { - $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' - - $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' - $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ - InstanceName = 'MSSQLSERVER' - } - # Mock the resource to be online. - $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' - $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE2' -TypeName 'String' - - return $mock - } - - Mock -CommandName Get-CimAssociatedInstance - Mock -CommandName Invoke-CimMethod - } - - It 'Should not restart any cluster resources' { - InModuleScope -ScriptBlock { - $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') - } - - Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 - Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 0 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'TakeOffline' - } -Scope It -Exactly -Times 0 - - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { - $MethodName -eq 'BringOnline' - } -Scope It -Exactly -Times 0 - } - } - } -} - -Describe 'SqlServerDsc.Common\Connect-SQLAnalysis' -Tag 'ConnectSQLAnalysis' { - BeforeAll { - $mockInstanceName = 'TEST' - $mockDynamicConnectedStatus = $true - - $mockNewObject_MicrosoftAnalysisServicesServer = { - return New-Object -TypeName Object | - Add-Member -MemberType 'NoteProperty' -Name 'Connected' -Value $mockDynamicConnectedStatus -PassThru | - Add-Member -MemberType 'ScriptMethod' -Name 'Connect' -Value { - param - ( - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $DataSource - ) - - if ($DataSource -ne $mockExpectedDataSource) - { - throw ("Datasource was expected to be '{0}', but was '{1}'." -f $mockExpectedDataSource, $dataSource) - } - - if ($mockThrowInvalidOperation) - { - throw 'Unable to connect.' - } - } -PassThru -Force - } - - $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter = { - $TypeName -eq 'Microsoft.AnalysisServices.Server' - } - - $mockSqlCredentialUserName = 'TestUserName12345' - $mockSqlCredentialPassword = 'StrongOne7.' - $mockSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockSqlCredentialPassword -AsPlainText -Force - $mockSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockSqlCredentialUserName, $mockSqlCredentialSecurePassword) - - $mockNetBiosSqlCredentialUserName = 'DOMAIN\TestUserName12345' - $mockNetBiosSqlCredentialPassword = 'StrongOne7.' - $mockNetBiosSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockNetBiosSqlCredentialPassword -AsPlainText -Force - $mockNetBiosSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockNetBiosSqlCredentialUserName, $mockNetBiosSqlCredentialSecurePassword) - - $mockFqdnSqlCredentialUserName = 'TestUserName12345@domain.local' - $mockFqdnSqlCredentialPassword = 'StrongOne7.' - $mockFqdnSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockFqdnSqlCredentialPassword -AsPlainText -Force - $mockFqdnSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockFqdnSqlCredentialUserName, $mockFqdnSqlCredentialSecurePassword) - - $mockComputerName = Get-ComputerName - } - - BeforeEach { - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftAnalysisServicesServer ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - - Context 'When using feature flag ''AnalysisServicesConnection''' { - BeforeAll { - Mock -CommandName Import-SqlDscPreferredModule - - $mockExpectedDataSource = "Data Source=$mockComputerName" - } - - Context 'When connecting to the default instance using Windows Authentication' { - It 'Should not throw when connecting' { - $null = Connect-SQLAnalysis -FeatureFlag 'AnalysisServicesConnection' - - Should -Invoke -CommandName Import-SqlDscPreferredModule -Exactly -Times 1 -Scope It - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - - Context 'When Connected status is $false' { - BeforeAll { - $mockDynamicConnectedStatus = $false - } - - AfterAll { - $mockDynamicConnectedStatus = $true - } - - It 'Should throw the correct error' { - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.FailedToConnectToAnalysisServicesInstance - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f $mockComputerName - ) - - { Connect-SQLAnalysis -FeatureFlag 'AnalysisServicesConnection' } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - } - } - } - - Context 'When connecting to the named instance using Windows Authentication' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName" - - $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -FeatureFlag 'AnalysisServicesConnection' - } - } - - Context 'When connecting to the named instance using Windows Authentication impersonation' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockSqlCredentialUserName;Password=$mockSqlCredentialPassword" - - $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockSqlCredential -FeatureFlag 'AnalysisServicesConnection' - } - } - } - - Context 'When not using feature flag ''AnalysisServicesConnection''' { - BeforeAll { - Mock -CommandName Import-Assembly - } - - Context 'When connecting to the default instance using Windows Authentication' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName" - - $null = Connect-SQLAnalysis - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - - Context 'When connecting to the named instance using Windows Authentication' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName" - - $null = Connect-SQLAnalysis -InstanceName $mockInstanceName - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - - Context 'When connecting to the named instance using Windows Authentication impersonation' { - Context 'When authentication without NetBIOS domain and Fully Qualified Domain Name (FQDN)' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockSqlCredentialUserName;Password=$mockSqlCredentialPassword" - - $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockSqlCredential - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - - Context 'When authentication using NetBIOS domain' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockNetBiosSqlCredentialUserName;Password=$mockNetBiosSqlCredentialPassword" - - $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockNetBiosSqlCredential - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - - Context 'When authentication using Fully Qualified Domain Name (FQDN)' { - It 'Should not throw when connecting' { - $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockFqdnSqlCredentialUserName;Password=$mockFqdnSqlCredentialPassword" - - $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockFqdnSqlCredential - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - } - - Context 'When connecting to the default instance using the correct service instance but does not return a correct Analysis Service object' { - It 'Should throw the correct error' { - $mockExpectedDataSource = '' - - Mock -CommandName New-Object ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.FailedToConnectToAnalysisServicesInstance - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f $mockComputerName - ) - - { Connect-SQLAnalysis } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - - Context 'When connecting to the default instance using a Analysis Service instance that does not exist' { - It 'Should throw the correct error' { - $mockExpectedDataSource = "Data Source=$mockComputerName" - - # Force the mock of Connect() method to throw 'Unable to connect.' - $mockThrowInvalidOperation = $true - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.FailedToConnectToAnalysisServicesInstance - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f $mockComputerName - ) - - { Connect-SQLAnalysis } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - - # Setting it back to the default so it does not disturb other tests. - $mockThrowInvalidOperation = $false - } - } - - # This test is to test the mock so that it throws correct when data source is not the expected data source - Context 'When connecting to the named instance using another data source then expected' { - It 'Should throw the correct error' { - $mockExpectedDataSource = 'Force wrong data source' - - $testParameters = @{ - ServerName = 'DummyHost' - InstanceName = $mockInstanceName - } - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.FailedToConnectToAnalysisServicesInstance - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f "$($testParameters.ServerName)\$($testParameters.InstanceName)" - ) - - { Connect-SQLAnalysis @testParameters } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter - } - } - } -} - -Describe 'SqlServerDsc.Common\Update-AvailabilityGroupReplica' -Tag 'UpdateAvailabilityGroupReplica' { - Context 'When the Availability Group Replica is altered' { - It 'Should silently alter the Availability Group Replica' { - $availabilityReplica = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityReplica - - $null = Update-AvailabilityGroupReplica -AvailabilityGroupReplica $availabilityReplica - } - - It 'Should throw the correct error, AlterAvailabilityGroupReplicaFailed, when altering the Availability Group Replica fails' { - $availabilityReplica = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityReplica - $availabilityReplica.Name = 'AlterFailed' - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.AlterAvailabilityGroupReplicaFailed - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $mockLocalizedString -f $availabilityReplica.Name - ) - - $mockErrorRecord.Exception.Message | Should -Not -BeNullOrEmpty - - { Update-AvailabilityGroupReplica -AvailabilityGroupReplica $availabilityReplica } | - Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - } - } -} - -Describe 'SqlServerDsc.Common\Test-LoginEffectivePermissions' -Tag 'TestLoginEffectivePermissions' { - BeforeAll { - $mockAllServerPermissionsPresent = @( - 'Connect SQL', - 'Alter Any Availability Group', - 'View Server State' - ) - - $mockServerPermissionsMissing = @( - 'Connect SQL', - 'View Server State' - ) - - $mockAllLoginPermissionsPresent = @( - 'View Definition', - 'Impersonate' - ) - - $mockLoginPermissionsMissing = @( - 'View Definition' - ) - - $mockInvokeQueryPermissionsSet = @() # Will be set dynamically in the check - - $mockInvokeQueryPermissionsResult = { - return New-Object -TypeName PSObject -Property @{ - Tables = @{ - Rows = @{ - permission_name = $mockInvokeQueryPermissionsSet - } - } - } - } - - $testLoginEffectiveServerPermissionsParams = @{ - ServerName = 'Server1' - InstanceName = 'MSSQLSERVER' - Login = 'NT SERVICE\ClusSvc' - Permissions = @() - } - - $testLoginEffectiveLoginPermissionsParams = @{ - ServerName = 'Server1' - InstanceName = 'MSSQLSERVER' - Login = 'NT SERVICE\ClusSvc' - Permissions = @() - SecurableClass = 'LOGIN' - SecurableName = 'Login1' - } - } - - BeforeEach { - Mock -CommandName Invoke-SqlDscQuery -MockWith $mockInvokeQueryPermissionsResult - } - - Context 'When all of the permissions are present' { - It 'Should return $true when the desired server permissions are present' { - $mockInvokeQueryPermissionsSet = $mockAllServerPermissionsPresent.Clone() - $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() - - Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -BeTrue - - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - - It 'Should return $true when the desired login permissions are present' { - $mockInvokeQueryPermissionsSet = $mockAllLoginPermissionsPresent.Clone() - $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() - - Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -BeTrue - - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - } - - Context 'When a permission is missing' { - It 'Should return $false when the desired server permissions are not present' { - $mockInvokeQueryPermissionsSet = $mockServerPermissionsMissing.Clone() - $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() - - Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -BeFalse - - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - - It 'Should return $false when the specified login has no server permissions assigned' { - $mockInvokeQueryPermissionsSet = @() - $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() - - Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -BeFalse - - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - - It 'Should return $false when the desired login permissions are not present' { - $mockInvokeQueryPermissionsSet = $mockLoginPermissionsMissing.Clone() - $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() - - Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -BeFalse - - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - - It 'Should return $false when the specified login has no login permissions assigned' { - $mockInvokeQueryPermissionsSet = @() - $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() - - Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -BeFalse - - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - } -} - -Describe 'SqlServerDsc.Common\Get-SqlInstanceMajorVersion' -Tag 'GetSqlInstanceMajorVersion' { - BeforeAll { - $mockSqlMajorVersion = 13 - $mockInstanceName = 'TEST' - - $mockGetItemProperty_MicrosoftSQLServer_InstanceNames_SQL = { - return @( - ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name $mockInstanceName -Value $mockInstance_InstanceId -PassThru -Force - ) - ) - } - - $mockGetItemProperty_MicrosoftSQLServer_FullInstanceId_Setup = { - return @( - ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'Version' -Value "$($mockSqlMajorVersion).0.4001.0" -PassThru -Force - ) - ) - } - - $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL = { - $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' - } - - $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup = { - $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$mockInstance_InstanceId\Setup" - } - } - - BeforeEach { - Mock -CommandName Get-ItemProperty ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL ` - -MockWith $mockGetItemProperty_MicrosoftSQLServer_InstanceNames_SQL - - Mock -CommandName Get-ItemProperty ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup ` - -MockWith $mockGetItemProperty_MicrosoftSQLServer_FullInstanceId_Setup - } - - $mockInstance_InstanceId = "MSSQL$($mockSqlMajorVersion).$($mockInstanceName)" - - Context 'When calling Get-SqlInstanceMajorVersion' { - It 'Should return the correct major SQL version number' { - $result = Get-SqlInstanceMajorVersion -InstanceName $mockInstanceName - $result | Should -Be $mockSqlMajorVersion - - Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL - - Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup - } - } - - Context 'When calling Get-SqlInstanceMajorVersion and nothing is returned' { - It 'Should throw the correct error' { - Mock -CommandName Get-ItemProperty ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup ` - -MockWith { - return New-Object -TypeName Object - } - - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.SqlServerVersionIsInvalid - } - - $mockErrorMessage = Get-InvalidResultRecord -Message ( - $mockLocalizedString -f $mockInstanceName - ) - - $mockErrorMessage | Should -Not -BeNullOrEmpty - - { Get-SqlInstanceMajorVersion -InstanceName $mockInstanceName } | Should -Throw -ExpectedMessage $mockErrorMessage - - Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL - - Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup - } - } -} - -Describe 'SqlServerDsc.Common\Get-PrimaryReplicaServerObject' -Tag 'GetPrimaryReplicaServerObject' { - BeforeEach { - $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server - $mockServerObject.DomainInstanceName = 'Server1' - - $mockAvailabilityGroup = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityGroup - $mockAvailabilityGroup.PrimaryReplicaServerName = 'Server1' - - $mockConnectSql = { - param - ( - [Parameter()] - [System.String] - $ServerName, - - [Parameter()] - [System.String] - $InstanceName - ) - - $mock = @( - ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'DomainInstanceName' -Value $ServerName -PassThru - ) - ) - - # Type the mock as a server object - $mock.PSObject.TypeNames.Insert(0, 'Microsoft.SqlServer.Management.Smo.Server') - - return $mock - } - - Mock -CommandName Connect-SQL -MockWith $mockConnectSql - } - - Context 'When the supplied server object is the primary replica' { - It 'Should return the same server object that was supplied' { - $result = Get-PrimaryReplicaServerObject -ServerObject $mockServerObject -AvailabilityGroup $mockAvailabilityGroup - - $result.DomainInstanceName | Should -Be $mockServerObject.DomainInstanceName - $result.DomainInstanceName | Should -Be $mockAvailabilityGroup.PrimaryReplicaServerName - - Should -Invoke -CommandName Connect-SQL -Scope It -Times 0 -Exactly - } - - It 'Should return the same server object that was supplied when the PrimaryReplicaServerNameProperty is empty' { - $mockAvailabilityGroup.PrimaryReplicaServerName = '' - - $result = Get-PrimaryReplicaServerObject -ServerObject $mockServerObject -AvailabilityGroup $mockAvailabilityGroup - - $result.DomainInstanceName | Should -Be $mockServerObject.DomainInstanceName - $result.DomainInstanceName | Should -Not -Be $mockAvailabilityGroup.PrimaryReplicaServerName - - Should -Invoke -CommandName Connect-SQL -Scope It -Times 0 -Exactly - } - } - - Context 'When the supplied server object is not the primary replica' { - It 'Should the server object of the primary replica' { - $mockAvailabilityGroup.PrimaryReplicaServerName = 'Server2' - - $result = Get-PrimaryReplicaServerObject -ServerObject $mockServerObject -AvailabilityGroup $mockAvailabilityGroup - - $result.DomainInstanceName | Should -Not -Be $mockServerObject.DomainInstanceName - $result.DomainInstanceName | Should -Be $mockAvailabilityGroup.PrimaryReplicaServerName - - Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly - } - } -} - -Describe 'SqlServerDsc.Common\Test-AvailabilityReplicaSeedingModeAutomatic' -Tag 'TestAvailabilityReplicaSeedingModeAutomatic' { - BeforeAll { - $mockConnectSql = { - $mock = @( - ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'Version' -Value $mockSqlVersion -PassThru - ) - ) - - # Type the mock as a server object - $mock.PSObject.TypeNames.Insert(0, 'Microsoft.SqlServer.Management.Smo.Server') - - return $mock - } - - $mockDynamic_SeedingMode = 'Manual' - $mockInvokeQuery = { - return @{ - Tables = @{ - Rows = @{ - seeding_mode_desc = $mockDynamic_SeedingMode - } - } - } - } - - $testAvailabilityReplicaSeedingModeAutomaticParams = @{ - ServerName = 'Server1' - InstanceName = 'MSSQLSERVER' - AvailabilityGroupName = 'Group1' - AvailabilityReplicaName = 'Replica2' - } - } - - Context 'When the replica seeding mode is manual' { - BeforeEach { - Mock -CommandName Connect-SQL -MockWith $mockConnectSql - Mock -CommandName Invoke-SqlDscQuery -MockWith $mockInvokeQuery - } - - It 'Should return $false when the instance version is <_>' -ForEach @(11, 12) { - $mockSqlVersion = $_ - - Test-AvailabilityReplicaSeedingModeAutomatic @testAvailabilityReplicaSeedingModeAutomaticParams | Should -BeFalse - - Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 0 -Exactly - } - - # Test SQL 2016 and later where Seeding Mode is supported. - It 'Should return $false when the instance version is <_> and the replica seeding mode is manual' -ForEach @(13, 14, 15) { - $mockSqlVersion = $_ - - Test-AvailabilityReplicaSeedingModeAutomatic @testAvailabilityReplicaSeedingModeAutomaticParams | Should -BeFalse - - Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - } - - Context 'When the replica seeding mode is automatic' { - BeforeEach { - Mock -CommandName Connect-SQL -MockWith $mockConnectSql - Mock -CommandName Invoke-SqlDscQuery -MockWith $mockInvokeQuery - } - - # Test SQL 2016 and later where Seeding Mode is supported. - It 'Should return $true when the instance version is <_> and the replica seeding mode is automatic' -ForEach @(13, 14, 15) { - $mockSqlVersion = $_ - $mockDynamic_SeedingMode = 'Automatic' - - Test-AvailabilityReplicaSeedingModeAutomatic @testAvailabilityReplicaSeedingModeAutomaticParams | Should -BeTrue - - Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly - } - } -} - -Describe 'SqlServerDsc.Common\Test-ImpersonatePermissions' -Tag 'TestImpersonatePermissions' { - BeforeAll { - $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter = { - $Permissions -eq @('IMPERSONATE ANY LOGIN') - } - - $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter = { - $Permissions -eq @('CONTROL SERVER') - } - - $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter = { - $Permissions -eq @('IMPERSONATE') - } - - $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter = { - $Permissions -eq @('CONTROL') - } - - $mockConnectionContextObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ServerConnection - $mockConnectionContextObject.TrueLogin = 'Login1' - - $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server - $mockServerObject.ComputerNamePhysicalNetBIOS = 'Server1' - $mockServerObject.ServiceName = 'MSSQLSERVER' - $mockServerObject.ConnectionContext = $mockConnectionContextObject - } - - BeforeEach { - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -MockWith { $false } - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -MockWith { $false } - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -MockWith { $false } - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -MockWith { $false } - } - - Context 'When impersonate permissions are present for the login' { - It 'Should return true when the impersonate any login permissions are present for the login' { - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -MockWith { $true } - Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -BeTrue - - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly - } - - It 'Should return true when the control server permissions are present for the login' { - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -MockWith { $true } - Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -BeTrue - - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly - } - - It 'Should return true when the impersonate login permissions are present for the login' { - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -MockWith { $true } - Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -BeTrue - - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 1 -Exactly - } - - It 'Should return true when the control login permissions are present for the login' { - Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -MockWith { $true } - Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -BeTrue - - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 1 -Exactly - } - } - - Context 'When impersonate permissions are missing for the login' { - It 'Should return false when the server permissions are missing for the login' { - Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -BeFalse - - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 0 -Exactly - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 0 -Exactly - } - - It 'Should return false when the login permissions are missing for the login' { - Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -BeFalse - - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 1 -Exactly - Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 1 -Exactly - } - } -} - -Describe 'SqlServerDsc.Common\Connect-SQL' -Tag 'ConnectSql' { - BeforeEach { - $mockNewObject_MicrosoftDatabaseEngine = { - <# - $ArgumentList[0] will contain the ServiceInstance when calling mock New-Object. - But since the mock New-Object will also be called without arguments, we first - have to evaluate if $ArgumentList contains values. - #> - if ( $ArgumentList.Count -gt 0) - { - $serverInstance = $ArgumentList[0] - } - - return New-Object -TypeName Object | - Add-Member -MemberType ScriptProperty -Name Status -Value { - if ($mockExpectedDatabaseEngineInstance -eq 'MSSQLSERVER') - { - $mockExpectedServiceInstance = $mockExpectedDatabaseEngineServer - } - else - { - $mockExpectedServiceInstance = "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - } - - if ( $this.ConnectionContext.ServerInstance -eq $mockExpectedServiceInstance ) - { - return 'Online' - } - else - { - return $null - } - } -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value $serverInstance -PassThru | - Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name Login -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name SecurePassword -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUser -Value $false -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUserPassword -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUserName -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name EncryptConnection -Value $false -PassThru | - Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { - return $true - } -PassThru | - Add-Member -MemberType ScriptMethod -Name Connect -Value { - if ($mockExpectedDatabaseEngineInstance -eq 'MSSQLSERVER') - { - $mockExpectedServiceInstance = $mockExpectedDatabaseEngineServer - } - else - { - $mockExpectedServiceInstance = "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - } - - if ($this.serverInstance -ne $mockExpectedServiceInstance) - { - throw ("Mock method Connect() was expecting ServerInstance to be '{0}', but was '{1}'." -f $mockExpectedServiceInstance, $this.serverInstance ) - } - - if ($mockThrowInvalidOperation) - { - throw 'Unable to connect.' - } - } -PassThru -Force - ) -PassThru -Force - } - - $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter = { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' - } - - $mockSqlCredentialUserName = 'TestUserName12345' - $mockSqlCredentialPassword = 'StrongOne7.' - $mockSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockSqlCredentialPassword -AsPlainText -Force - $mockSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockSqlCredentialUserName, $mockSqlCredentialSecurePassword) - - $mockWinCredentialUserName = 'DOMAIN\TestUserName12345' - $mockWinCredentialPassword = 'StrongerOne7.' - $mockWinCredentialSecurePassword = ConvertTo-SecureString -String $mockWinCredentialPassword -AsPlainText -Force - $mockWinCredential = New-Object -TypeName PSCredential -ArgumentList ($mockWinCredentialUserName, $mockWinCredentialSecurePassword) - - $mockWinFqdnCredentialUserName = 'TestUserName12345@domain.local' - $mockWinFqdnCredentialPassword = 'StrongerOne7.' - $mockWinFqdnCredentialSecurePassword = ConvertTo-SecureString -String $mockWinFqdnCredentialPassword -AsPlainText -Force - $mockWinFqdnCredential = New-Object -TypeName PSCredential -ArgumentList ($mockWinFqdnCredentialUserName, $mockWinFqdnCredentialSecurePassword) - - Mock -CommandName Import-SqlDscPreferredModule - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - Context 'When connecting to the default instance using integrated Windows Authentication' -Skip:($IsLinux -or $IsMacOS) { - BeforeEach { - $mockExpectedDatabaseEngineServer = 'TestServer' - $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly $mockExpectedDatabaseEngineServer - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - Context 'When connecting to the default instance using SQL Server Authentication' { - BeforeEach { - $mockExpectedDatabaseEngineServer = 'TestServer' - $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' - $mockExpectedDatabaseEngineLoginSecure = $false - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -SetupCredential $mockSqlCredential -LoginType 'SqlLogin' -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeFalse - $databaseEngineServerObject.ConnectionContext.Login | Should -Be $mockSqlCredentialUserName - $databaseEngineServerObject.ConnectionContext.SecurePassword | Should -Be $mockSqlCredentialSecurePassword - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly $mockExpectedDatabaseEngineServer - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - Context 'When connecting to the named instance using integrated Windows Authentication' -Skip:($IsLinux -or $IsMacOS) { - BeforeEach { - $mockExpectedDatabaseEngineServer = Get-ComputerName - $mockExpectedDatabaseEngineInstance = 'SqlInstance' - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL -InstanceName $mockExpectedDatabaseEngineInstance -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - Context 'When connecting to the named instance using SQL Server Authentication' { - BeforeEach { - $mockExpectedDatabaseEngineServer = Get-ComputerName - $mockExpectedDatabaseEngineInstance = 'SqlInstance' - $mockExpectedDatabaseEngineLoginSecure = $false - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL -InstanceName $mockExpectedDatabaseEngineInstance -SetupCredential $mockSqlCredential -LoginType 'SqlLogin' -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeFalse - $databaseEngineServerObject.ConnectionContext.Login | Should -Be $mockSqlCredentialUserName - $databaseEngineServerObject.ConnectionContext.SecurePassword | Should -Be $mockSqlCredentialSecurePassword - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - Context 'When connecting to the named instance using integrated Windows Authentication and different server name' -Skip:($IsLinux -or $IsMacOS) { - BeforeEach { - $mockExpectedDatabaseEngineServer = 'SERVER' - $mockExpectedDatabaseEngineInstance = 'SqlInstance' - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName $mockExpectedDatabaseEngineInstance -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - Context 'When connecting to the named instance using Windows Authentication impersonation' { - BeforeEach { - $mockExpectedDatabaseEngineServer = Get-ComputerName - $mockExpectedDatabaseEngineInstance = 'SqlInstance' - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - Context 'When using the default login type' { - BeforeEach { - $testParameters = @{ - ServerName = $mockExpectedDatabaseEngineServer - InstanceName = $mockExpectedDatabaseEngineInstance - SetupCredential = $mockWinCredential - } - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL @testParameters -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue - $databaseEngineServerObject.ConnectionContext.ConnectAsUserPassword | Should -BeExactly $mockWinCredential.GetNetworkCredential().Password - $databaseEngineServerObject.ConnectionContext.ConnectAsUserName | Should -BeExactly $mockWinCredential.UserName - $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue - $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeTrue - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - Context 'When using the WindowsUser login type' { - Context 'When authenticating using NetBIOS domain' { - BeforeEach { - $testParameters = @{ - ServerName = $mockExpectedDatabaseEngineServer - InstanceName = $mockExpectedDatabaseEngineInstance - SetupCredential = $mockWinCredential - LoginType = 'WindowsUser' - } - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL @testParameters -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue - $databaseEngineServerObject.ConnectionContext.ConnectAsUserPassword | Should -BeExactly $mockWinCredential.GetNetworkCredential().Password - $databaseEngineServerObject.ConnectionContext.ConnectAsUserName | Should -BeExactly $mockWinCredential.UserName - $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue - $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeTrue - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - Context 'When authenticating using Fully Qualified Domain Name (FQDN)' { - BeforeEach { - $testParameters = @{ - ServerName = $mockExpectedDatabaseEngineServer - InstanceName = $mockExpectedDatabaseEngineInstance - SetupCredential = $mockWinFqdnCredential - LoginType = 'WindowsUser' - } - } - - It 'Should return the correct service instance' { - $databaseEngineServerObject = Connect-SQL @testParameters -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue - $databaseEngineServerObject.ConnectionContext.ConnectAsUserPassword | Should -BeExactly $mockWinFqdnCredential.GetNetworkCredential().Password - $databaseEngineServerObject.ConnectionContext.ConnectAsUserName | Should -BeExactly $mockWinFqdnCredential.UserName - $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue - $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeTrue - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - } - } - - Context 'When using encryption' { - BeforeEach { - $mockExpectedDatabaseEngineServer = 'SERVER' - $mockExpectedDatabaseEngineInstance = 'SqlInstance' - - Mock -CommandName New-Object ` - -MockWith $mockNewObject_MicrosoftDatabaseEngine ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should return the correct service instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -Encrypt -ServerName $mockExpectedDatabaseEngineServer -InstanceName $mockExpectedDatabaseEngineInstance -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" - - Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` - -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter - } - } - - Context 'When connecting using Protocol parameter' { - BeforeEach { - $mockExpectedDatabaseEngineServer = 'TestServer' - $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' - - # Mock that expects protocol prefix in the ServerInstance - Mock -CommandName New-Object -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter -MockWith { - return New-Object -TypeName Object | - Add-Member -MemberType ScriptProperty -Name Status -Value { - return 'Online' - } -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | - Add-Member -MemberType ScriptMethod -Name Connect -Value { } -PassThru -Force - ) -PassThru -Force - } - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should format the connection string with tcp protocol prefix for default instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -Protocol 'tcp' -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer" - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should format the connection string with tcp protocol prefix for named instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName 'MyInstance' -Protocol 'tcp' -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer\MyInstance" - } - } - - Context 'When connecting using Port parameter' { - BeforeEach { - $mockExpectedDatabaseEngineServer = 'TestServer' - $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' - - # Mock that expects port suffix in the ServerInstance - Mock -CommandName New-Object -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter -MockWith { - return New-Object -TypeName Object | - Add-Member -MemberType ScriptProperty -Name Status -Value { - return 'Online' - } -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | - Add-Member -MemberType ScriptMethod -Name Connect -Value { } -PassThru -Force - ) -PassThru -Force - } - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should format the connection string with port for default instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -Port 1433 -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer,1433" - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should format the connection string with port for named instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName 'MyInstance' -Port 50200 -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\MyInstance,50200" - } - } - - Context 'When connecting using both Protocol and Port parameters' { - BeforeEach { - $mockExpectedDatabaseEngineServer = '192.168.1.1' - $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' - - # Mock that expects protocol prefix and port suffix in the ServerInstance - Mock -CommandName New-Object -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter -MockWith { - return New-Object -TypeName Object | - Add-Member -MemberType ScriptProperty -Name Status -Value { - return 'Online' - } -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | - Add-Member -MemberType ScriptMethod -Name Connect -Value { } -PassThru -Force - ) -PassThru -Force - } - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should format the connection string with protocol and port for default instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -Protocol 'tcp' -Port 1433 -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer,1433" - } - - # Skipping on Linux and macOS because they do not support Windows Authentication. - It 'Should format the connection string with protocol and port for named instance' -Skip:($IsLinux -or $IsMacOS) { - $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName 'MyInstance' -Protocol 'tcp' -Port 50200 -ErrorAction 'Stop' - $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer\MyInstance,50200" - } - } - - Context 'When connecting to the default instance using the correct service instance but does not return a correct Database Engine object' { - Context 'When using ErrorAction set to Stop' -Skip:($IsLinux -or $IsMacOS) { - BeforeAll { - Mock -CommandName New-Object -ParameterFilter { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' - } -MockWith { - return New-Object -TypeName Object | - Add-Member -MemberType ScriptProperty -Name Status -Value { - return $null - } -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value 'localhost' -PassThru | - Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name Login -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name SecurePassword -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUser -Value $false -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUserPassword -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUserName -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { - return $true - } -PassThru | - Add-Member -MemberType ScriptMethod -Name Connect -Value { - return - } -PassThru -Force - ) -PassThru -Force - } - } - - It 'Should throw the correct error' { - $mockLocalizedString = InModuleScope -ScriptBlock { - $script:localizedData.FailedToConnectToDatabaseEngineInstance - } - - $mockErrorMessage = $mockLocalizedString -f 'localhost' - - { Connect-SQL -ServerName 'localhost' -ErrorAction 'Stop' } | - Should -Throw -ExpectedMessage $mockErrorMessage - - Should -Invoke -CommandName New-Object -ParameterFilter { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' - } -Exactly -Times 1 -Scope It - } - } - - Context 'When using ErrorAction set to SilentlyContinue' { - BeforeAll { - Mock -CommandName New-Object -ParameterFilter { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' - } -MockWith { - return New-Object -TypeName Object | - Add-Member -MemberType ScriptProperty -Name Status -Value { - return $null - } -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name ServerInstance -Value 'localhost' -PassThru | - Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name Login -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name SecurePassword -Value $null -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUser -Value $false -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUserPassword -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectAsUserName -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | - Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | - Add-Member -MemberType ScriptMethod -Name Disconnect -Value { - return $true - } -PassThru | - Add-Member -MemberType ScriptMethod -Name Connect -Value { - return - } -PassThru -Force - ) -PassThru -Force - } - } - - It 'Should not throw an exception' { - $null = Connect-SQL -ServerName 'localhost' -SetupCredential $mockSqlCredential -LoginType 'SqlLogin' -ErrorAction 'SilentlyContinue' - - Should -Invoke -CommandName New-Object -ParameterFilter { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' - } -Exactly -Times 1 -Scope It - } - } - } -} - -Describe 'SqlServerDsc.Common\Split-FullSqlInstanceName' -Tag 'SplitFullSqlInstanceName' { - Context 'When the "FullSqlInstanceName" parameter is not supplied' { - It 'Should throw when the "FullSqlInstanceName" parameter is $null' { - { Split-FullSqlInstanceName -FullSqlInstanceName $null } | Should -Throw - } - - It 'Should throw when the "FullSqlInstanceName" parameter is an empty string' { - { Split-FullSqlInstanceName -FullSqlInstanceName '' } | Should -Throw - } - } - - Context 'When the "FullSqlInstanceName" parameter is supplied' { - It 'Should throw when the "FullSqlInstanceName" parameter is "ServerName"' { - $result = Split-FullSqlInstanceName -FullSqlInstanceName 'ServerName' - - $result.Count | Should -Be 2 - $result.ServerName | Should -Be 'ServerName' - $result.InstanceName | Should -Be 'MSSQLSERVER' - } - - It 'Should throw when the "FullSqlInstanceName" parameter is "ServerName\InstanceName"' { - $result = Split-FullSqlInstanceName -FullSqlInstanceName 'ServerName\InstanceName' - - $result.Count | Should -Be 2 - $result.ServerName | Should -Be 'ServerName' - $result.InstanceName | Should -Be 'InstanceName' - } - } -} - -Describe 'SqlServerDsc.Common\Test-ClusterPermissions' -Tag 'TestClusterPermissions' { - BeforeAll { - Mock -CommandName Test-LoginEffectivePermissions -MockWith { - $mockClusterServicePermissionsPresent - } -ParameterFilter { - $LoginName -eq $clusterServiceName - } - - Mock -CommandName Test-LoginEffectivePermissions -MockWith { - $mockSystemPermissionsPresent - } -ParameterFilter { - $LoginName -eq $systemAccountName - } - - $clusterServiceName = 'NT SERVICE\ClusSvc' - $systemAccountName = 'NT AUTHORITY\System' - } - - BeforeEach { - $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server - $mockServerObject.NetName = 'TestServer' - $mockServerObject.ServiceName = 'MSSQLSERVER' - - $mockLogins = @{ - $clusterServiceName = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Login -ArgumentList $mockServerObject, $clusterServiceName - $systemAccountName = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Login -ArgumentList $mockServerObject, $systemAccountName - } - - $mockServerObject.Logins = $mockLogins - - $mockClusterServicePermissionsPresent = $false - $mockSystemPermissionsPresent = $false - } - - Context 'When the cluster does not have permissions to the instance' { - It "Should throw the correct error when the logins '$($clusterServiceName)' or '$($systemAccountName)' are absent" { - $mockServerObject.Logins = @{} - - { Test-ClusterPermissions -ServerObject $mockServerObject } | Should -Throw -ExpectedMessage ( "The cluster does not have permissions to manage the Availability Group on '{0}\{1}'. Grant 'Connect SQL', 'Alter Any Availability Group', and 'View Server State' to either '$($clusterServiceName)' or '$($systemAccountName)'. (SQLCOMMON0049)" -f $mockServerObject.NetName, $mockServerObject.ServiceName ) - - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 0 -Exactly -ParameterFilter { - $LoginName -eq $clusterServiceName - } - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 0 -Exactly -ParameterFilter { - $LoginName -eq $systemAccountName - } - } - - It "Should throw the correct error when the logins '$($clusterServiceName)' and '$($systemAccountName)' do not have permissions to manage availability groups" { - { Test-ClusterPermissions -ServerObject $mockServerObject } | Should -Throw -ExpectedMessage ( "The cluster does not have permissions to manage the Availability Group on '{0}\{1}'. Grant 'Connect SQL', 'Alter Any Availability Group', and 'View Server State' to either '$($clusterServiceName)' or '$($systemAccountName)'. (SQLCOMMON0049)" -f $mockServerObject.NetName, $mockServerObject.ServiceName ) - - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { - $LoginName -eq $clusterServiceName - } - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { - $LoginName -eq $systemAccountName - } - } - } - - Context 'When the cluster has permissions to the instance' { - It "Should return NullOrEmpty when 'NT SERVICE\ClusSvc' is present and has the permissions to manage availability groups" { - $mockClusterServicePermissionsPresent = $true - - Test-ClusterPermissions -ServerObject $mockServerObject | Should -BeTrue - - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { - $LoginName -eq $clusterServiceName - } - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 0 -Exactly -ParameterFilter { - $LoginName -eq $systemAccountName - } - } - - It "Should return NullOrEmpty when 'NT AUTHORITY\System' is present and has the permissions to manage availability groups" { - $mockSystemPermissionsPresent = $true - - Test-ClusterPermissions -ServerObject $mockServerObject | Should -BeTrue - - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { - $LoginName -eq $clusterServiceName - } - Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { - $LoginName -eq $systemAccountName - } - } - } -} - -Describe 'SqlServerDsc.Common\Test-ActiveNode' -Tag 'TestActiveNode' { - BeforeAll { - $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server - } - - Context 'When function is executed on a standalone instance' { - BeforeAll { - $mockServerObject.IsMemberOfWsfcCluster = $false - } - - It 'Should return $true' { - Test-ActiveNode -ServerObject $mockServerObject | Should -BeTrue - } - } - - Context 'When function is executed on a failover cluster instance (FCI)' { - BeforeAll { - $mockServerObject.IsMemberOfWsfcCluster = $true - } - - It 'Should return when the node name is ' -ForEach @( - @{ - ComputerNamePhysicalNetBIOS = Get-ComputerName - Result = $true - }, - @{ - ComputerNamePhysicalNetBIOS = 'AnotherNode' - Result = $false - } - ) { - $mockServerObject.ComputerNamePhysicalNetBIOS = $ComputerNamePhysicalNetBIOS - - Test-ActiveNode -ServerObject $mockServerObject | Should -Be $Result - } - } -} - -Describe 'SqlServerDsc.Common\Invoke-SqlScript' -Tag 'InvokeSqlScript' { - BeforeAll { - $invokeScriptFileParameters = @{ - ServerInstance = Get-ComputerName - InputFile = 'set.sql' - } - - $invokeScriptQueryParameters = @{ - ServerInstance = Get-ComputerName - Query = 'Test Query' - } - } - - Context 'Invoke-SqlScript fails to import SQLPS module' { - BeforeAll { - $throwMessage = 'Failed to import SQLPS module.' - - Mock -CommandName Import-SqlDscPreferredModule -MockWith { - throw $throwMessage - } - } - - It 'Should throw the correct error from Import-Module' { - { Invoke-SqlScript @invokeScriptFileParameters } | Should -Throw -ExpectedMessage $throwMessage - } - } - - Context 'Invoke-SqlScript is called with credentials' { - BeforeAll { - # Import PowerShell module SqlServer stub cmdlets. - Import-SQLModuleStub - - $mockPasswordPlain = 'password' - $mockUsername = 'User' - - $password = ConvertTo-SecureString -String $mockPasswordPlain -AsPlainText -Force - $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $mockUsername, $password - - Mock -CommandName Import-SqlDscPreferredModule - Mock -CommandName Invoke-SqlCmd -ParameterFilter { - $Username -eq $mockUsername -and $Password -eq $mockPasswordPlain - } - } - - AfterAll { - # Remove PowerShell module SqlServer stub cmdlets. - Remove-SqlModuleStub - } - - It 'Should call Invoke-SqlCmd with correct File ParameterSet parameters' { - $invokeScriptFileParameters.Add('Credential', $credential) - $null = Invoke-SqlScript @invokeScriptFileParameters - - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $Username -eq $mockUsername -and $Password -eq $mockPasswordPlain - } -Times 1 -Exactly -Scope It - } - - It 'Should call Invoke-SqlCmd with correct Query ParameterSet parameters' { - $invokeScriptQueryParameters.Add('Credential', $credential) - $null = Invoke-SqlScript @invokeScriptQueryParameters - - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $Username -eq $mockUsername -and $Password -eq $mockPasswordPlain - } -Times 1 -Exactly -Scope It - } - } - - Context 'Invoke-SqlScript fails to execute the SQL scripts' { - BeforeAll { - # Import PowerShell module SqlServer stub cmdlets. - Import-SqlModuleStub - } - - AfterAll { - # Remove PowerShell module SqlServer stub cmdlets. - Remove-SqlModuleStub - } - - BeforeEach { - $errorMessage = 'Failed to run SQL Script' - - Mock -CommandName Import-SqlDscPreferredModule - Mock -CommandName Invoke-SqlCmd -MockWith { - throw $errorMessage - } - } - - It 'Should throw the correct error from File ParameterSet Invoke-SqlCmd' { - { Invoke-SqlScript @invokeScriptFileParameters } | Should -Throw -ExpectedMessage $errorMessage - } - - It 'Should throw the correct error from Query ParameterSet Invoke-SqlCmd' { - { Invoke-SqlScript @invokeScriptQueryParameters } | Should -Throw -ExpectedMessage $errorMessage - } - } - - Context 'Invoke-SqlScript is called with parameter Encrypt' { - BeforeAll { - # Import PowerShell module SqlServer stub cmdlets. - Import-SQLModuleStub - - Mock -CommandName Import-SqlDscPreferredModule - Mock -CommandName Invoke-SqlCmd - } - - AfterAll { - # Remove PowerShell module SqlServer stub cmdlets. - Remove-SqlModuleStub - } - - Context 'When using SqlServer module v22.x' { - BeforeAll { - Mock -CommandName Get-Command -ParameterFilter { - $Name -eq 'Invoke-SqlCmd' - } -MockWith { - return @{ - Parameters = @{ - Keys = @('Encrypt') - } - } - } - } - - It 'Should call Invoke-SqlCmd with correct File ParameterSet parameters' { - $mockInvokeScriptFileParameters = @{ - ServerInstance = Get-ComputerName - InputFile = 'set.sql' - Encrypt = 'Optional' - } - - $null = Invoke-SqlScript @mockInvokeScriptFileParameters - - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $Encrypt -eq 'Optional' - } -Times 1 -Exactly -Scope It - } - - It 'Should call Invoke-SqlCmd with correct Query ParameterSet parameters' { - $mockInvokeScriptQueryParameters = @{ - ServerInstance = Get-ComputerName - Query = 'Test Query' - Encrypt = 'Optional' - } - - $null = Invoke-SqlScript @mockInvokeScriptQueryParameters - - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $Encrypt -eq 'Optional' - } -Times 1 -Exactly -Scope It - } - } - - Context 'When using SqlServer module v21.x' { - BeforeAll { - Mock -CommandName Get-Command -ParameterFilter { - $Name -eq 'Invoke-SqlCmd' - } -MockWith { - return @{ - Parameters = @{ - Keys = @() - } - } - } - } - - It 'Should call Invoke-SqlCmd with correct File ParameterSet parameters' { - $mockInvokeScriptFileParameters = @{ - ServerInstance = Get-ComputerName - InputFile = 'set.sql' - Encrypt = 'Optional' - } - - $null = Invoke-SqlScript @mockInvokeScriptFileParameters - - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $PesterBoundParameters.Keys -notcontains 'Encrypt' - } -Times 1 -Exactly -Scope It - } - - It 'Should call Invoke-SqlCmd with correct Query ParameterSet parameters' { - $mockInvokeScriptQueryParameters = @{ - ServerInstance = Get-ComputerName - Query = 'Test Query' - Encrypt = 'Optional' - } - - $null = Invoke-SqlScript @mockInvokeScriptQueryParameters - - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $PesterBoundParameters.Keys -notcontains 'Encrypt' - } -Times 1 -Exactly -Scope It - } - } - } -} - -Describe 'SqlServerDsc.Common\Get-ServiceAccount' -Tag 'GetServiceAccount' { - BeforeAll { - $mockLocalSystemAccountUserName = 'NT AUTHORITY\SYSTEM' - $mockLocalSystemAccountCredential = New-Object System.Management.Automation.PSCredential $mockLocalSystemAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) - - $mockManagedServiceAccountUserName = 'CONTOSO\msa$' - $mockManagedServiceAccountCredential = New-Object System.Management.Automation.PSCredential $mockManagedServiceAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) - - $mockDomainAccountUserName = 'CONTOSO\User1' - $mockDomainAccountCredential = New-Object System.Management.Automation.PSCredential $mockDomainAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) - - $mockLocalServiceAccountUserName = 'NT SERVICE\MyService' - $mockLocalServiceAccountCredential = New-Object System.Management.Automation.PSCredential $mockLocalServiceAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) - } - - Context 'When getting service account' { - It 'Should return NT AUTHORITY\SYSTEM' { - $returnValue = Get-ServiceAccount -ServiceAccount $mockLocalSystemAccountCredential - - $returnValue.UserName | Should -Be $mockLocalSystemAccountUserName - $returnValue.Password | Should -BeNullOrEmpty - } - - It 'Should return Domain Account and Password' { - $returnValue = Get-ServiceAccount -ServiceAccount $mockDomainAccountCredential - - $returnValue.UserName | Should -Be $mockDomainAccountUserName - $returnValue.Password | Should -Be $mockDomainAccountCredential.GetNetworkCredential().Password - } - - It 'Should return managed service account' { - $returnValue = Get-ServiceAccount -ServiceAccount $mockManagedServiceAccountCredential - - $returnValue.UserName | Should -Be $mockManagedServiceAccountUserName - } - - It 'Should return local service account' { - $returnValue = Get-ServiceAccount -ServiceAccount $mockLocalServiceAccountCredential - - $returnValue.UserName | Should -Be $mockLocalServiceAccountUserName - $returnValue.Password | Should -BeNullOrEmpty - } - } -} - -Describe 'SqlServerDsc.Common\Find-ExceptionByNumber' -Tag 'FindExceptionByNumber' { - BeforeAll { - $mockInnerException = New-Object System.Exception 'This is a mock inner exception object' - $mockInnerException | Add-Member -Name 'Number' -Value 2 -MemberType NoteProperty - - $mockException = New-Object System.Exception 'This is a mock exception object', $mockInnerException - $mockException | Add-Member -Name 'Number' -Value 1 -MemberType NoteProperty - } - - Context 'When searching Exception objects' { - It 'Should return true for main exception' { - Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 1 | Should -BeTrue - } - - It 'Should return true for inner exception' { - Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 2 | Should -BeTrue - } - - It 'Should return false when message not found' { - Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 3 | Should -BeFalse - } - } -} - -Describe 'SqlServerDsc.Common\ConvertTo-ServerInstanceName' -Tag 'ConvertToServerInstanceName' { - BeforeAll { - $mockComputerName = Get-ComputerName - } - - It 'Should return correct service instance for a default instance' { - $result = ConvertTo-ServerInstanceName -InstanceName 'MSSQLSERVER' -ServerName $mockComputerName - - $result | Should -BeExactly $mockComputerName - } - - It 'Should return correct service instance for a name instance' { - $result = ConvertTo-ServerInstanceName -InstanceName 'MyInstance' -ServerName $mockComputerName - - $result | Should -BeExactly ('{0}\{1}' -f $mockComputerName, 'MyInstance') - } -} - -Describe 'Test-FeatureFlag' -Tag 'TestFeatureFlag' { - Context 'When no feature flags was provided' { - It 'Should return $false' { - Test-FeatureFlag -FeatureFlag $null -TestFlag 'MyFlag' | Should -BeFalse - } - } - - Context 'When feature flags was provided' { - It 'Should return $true' { - Test-FeatureFlag -FeatureFlag @('FirstFlag', 'SecondFlag') -TestFlag 'SecondFlag' | Should -BeTrue - } - } - - Context 'When feature flags was provided, but missing' { - It 'Should return $false' { - Test-FeatureFlag -FeatureFlag @('MyFlag2') -TestFlag 'MyFlag' | Should -BeFalse - } - } -} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Connect-Sql.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Connect-Sql.Tests.ps1 new file mode 100644 index 000000000..a834ade47 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Connect-Sql.Tests.ps1 @@ -0,0 +1,589 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Connect-SQL' -Tag 'ConnectSql' { + BeforeEach { + $mockNewObject_MicrosoftDatabaseEngine = { + <# + $ArgumentList[0] will contain the ServiceInstance when calling mock New-Object. + But since the mock New-Object will also be called without arguments, we first + have to evaluate if $ArgumentList contains values. + #> + if ( $ArgumentList.Count -gt 0) + { + $serverInstance = $ArgumentList[0] + } + + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name Status -Value { + if ($mockExpectedDatabaseEngineInstance -eq 'MSSQLSERVER') + { + $mockExpectedServiceInstance = $mockExpectedDatabaseEngineServer + } + else + { + $mockExpectedServiceInstance = "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + } + + if ( $this.ConnectionContext.ServerInstance -eq $mockExpectedServiceInstance ) + { + return 'Online' + } + else + { + return $null + } + } -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value $serverInstance -PassThru | + Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name Login -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name SecurePassword -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUser -Value $false -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUserPassword -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUserName -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name EncryptConnection -Value $false -PassThru | + Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { + return $true + } -PassThru | + Add-Member -MemberType ScriptMethod -Name Connect -Value { + if ($mockExpectedDatabaseEngineInstance -eq 'MSSQLSERVER') + { + $mockExpectedServiceInstance = $mockExpectedDatabaseEngineServer + } + else + { + $mockExpectedServiceInstance = "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + } + + if ($this.serverInstance -ne $mockExpectedServiceInstance) + { + throw ("Mock method Connect() was expecting ServerInstance to be '{0}', but was '{1}'." -f $mockExpectedServiceInstance, $this.serverInstance ) + } + + if ($mockThrowInvalidOperation) + { + throw 'Unable to connect.' + } + } -PassThru -Force + ) -PassThru -Force + } + + $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter = { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' + } + + $mockSqlCredentialUserName = 'TestUserName12345' + $mockSqlCredentialPassword = 'StrongOne7.' + $mockSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockSqlCredentialPassword -AsPlainText -Force + $mockSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockSqlCredentialUserName, $mockSqlCredentialSecurePassword) + + $mockWinCredentialUserName = 'DOMAIN\TestUserName12345' + $mockWinCredentialPassword = 'StrongerOne7.' + $mockWinCredentialSecurePassword = ConvertTo-SecureString -String $mockWinCredentialPassword -AsPlainText -Force + $mockWinCredential = New-Object -TypeName PSCredential -ArgumentList ($mockWinCredentialUserName, $mockWinCredentialSecurePassword) + + $mockWinFqdnCredentialUserName = 'TestUserName12345@domain.local' + $mockWinFqdnCredentialPassword = 'StrongerOne7.' + $mockWinFqdnCredentialSecurePassword = ConvertTo-SecureString -String $mockWinFqdnCredentialPassword -AsPlainText -Force + $mockWinFqdnCredential = New-Object -TypeName PSCredential -ArgumentList ($mockWinFqdnCredentialUserName, $mockWinFqdnCredentialSecurePassword) + + Mock -CommandName Import-SqlDscPreferredModule + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + Context 'When connecting to the default instance using integrated Windows Authentication' -Skip:($IsLinux -or $IsMacOS) { + BeforeEach { + $mockExpectedDatabaseEngineServer = 'TestServer' + $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly $mockExpectedDatabaseEngineServer + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When connecting to the default instance using SQL Server Authentication' { + BeforeEach { + $mockExpectedDatabaseEngineServer = 'TestServer' + $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + $mockExpectedDatabaseEngineLoginSecure = $false + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -SetupCredential $mockSqlCredential -LoginType 'SqlLogin' -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeFalse + $databaseEngineServerObject.ConnectionContext.Login | Should -Be $mockSqlCredentialUserName + $databaseEngineServerObject.ConnectionContext.SecurePassword | Should -Be $mockSqlCredentialSecurePassword + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly $mockExpectedDatabaseEngineServer + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + Context 'When connecting to the named instance using integrated Windows Authentication' -Skip:($IsLinux -or $IsMacOS) { + BeforeEach { + $mockExpectedDatabaseEngineServer = Get-ComputerName + $mockExpectedDatabaseEngineInstance = 'SqlInstance' + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL -InstanceName $mockExpectedDatabaseEngineInstance -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When connecting to the named instance using SQL Server Authentication' { + BeforeEach { + $mockExpectedDatabaseEngineServer = Get-ComputerName + $mockExpectedDatabaseEngineInstance = 'SqlInstance' + $mockExpectedDatabaseEngineLoginSecure = $false + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL -InstanceName $mockExpectedDatabaseEngineInstance -SetupCredential $mockSqlCredential -LoginType 'SqlLogin' -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeFalse + $databaseEngineServerObject.ConnectionContext.Login | Should -Be $mockSqlCredentialUserName + $databaseEngineServerObject.ConnectionContext.SecurePassword | Should -Be $mockSqlCredentialSecurePassword + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + Context 'When connecting to the named instance using integrated Windows Authentication and different server name' -Skip:($IsLinux -or $IsMacOS) { + BeforeEach { + $mockExpectedDatabaseEngineServer = 'SERVER' + $mockExpectedDatabaseEngineInstance = 'SqlInstance' + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName $mockExpectedDatabaseEngineInstance -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When connecting to the named instance using Windows Authentication impersonation' { + BeforeEach { + $mockExpectedDatabaseEngineServer = Get-ComputerName + $mockExpectedDatabaseEngineInstance = 'SqlInstance' + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + Context 'When using the default login type' { + BeforeEach { + $testParameters = @{ + ServerName = $mockExpectedDatabaseEngineServer + InstanceName = $mockExpectedDatabaseEngineInstance + SetupCredential = $mockWinCredential + } + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL @testParameters -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue + $databaseEngineServerObject.ConnectionContext.ConnectAsUserPassword | Should -BeExactly $mockWinCredential.GetNetworkCredential().Password + $databaseEngineServerObject.ConnectionContext.ConnectAsUserName | Should -BeExactly $mockWinCredential.UserName + $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue + $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeTrue + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When using the WindowsUser login type' { + Context 'When authenticating using NetBIOS domain' { + BeforeEach { + $testParameters = @{ + ServerName = $mockExpectedDatabaseEngineServer + InstanceName = $mockExpectedDatabaseEngineInstance + SetupCredential = $mockWinCredential + LoginType = 'WindowsUser' + } + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL @testParameters -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue + $databaseEngineServerObject.ConnectionContext.ConnectAsUserPassword | Should -BeExactly $mockWinCredential.GetNetworkCredential().Password + $databaseEngineServerObject.ConnectionContext.ConnectAsUserName | Should -BeExactly $mockWinCredential.UserName + $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue + $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeTrue + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When authenticating using Fully Qualified Domain Name (FQDN)' { + BeforeEach { + $testParameters = @{ + ServerName = $mockExpectedDatabaseEngineServer + InstanceName = $mockExpectedDatabaseEngineInstance + SetupCredential = $mockWinFqdnCredential + LoginType = 'WindowsUser' + } + } + + It 'Should return the correct service instance' { + $databaseEngineServerObject = Connect-SQL @testParameters -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue + $databaseEngineServerObject.ConnectionContext.ConnectAsUserPassword | Should -BeExactly $mockWinFqdnCredential.GetNetworkCredential().Password + $databaseEngineServerObject.ConnectionContext.ConnectAsUserName | Should -BeExactly $mockWinFqdnCredential.UserName + $databaseEngineServerObject.ConnectionContext.ConnectAsUser | Should -BeTrue + $databaseEngineServerObject.ConnectionContext.LoginSecure | Should -BeTrue + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + } + } + + Context 'When using encryption' { + BeforeEach { + $mockExpectedDatabaseEngineServer = 'SERVER' + $mockExpectedDatabaseEngineInstance = 'SqlInstance' + + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftDatabaseEngine ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should return the correct service instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -Encrypt -ServerName $mockExpectedDatabaseEngineServer -InstanceName $mockExpectedDatabaseEngineInstance -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\$mockExpectedDatabaseEngineInstance" + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter + } + } + + Context 'When connecting using Protocol parameter' { + BeforeEach { + $mockExpectedDatabaseEngineServer = 'TestServer' + $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + + # Mock that expects protocol prefix in the ServerInstance + Mock -CommandName New-Object -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter -MockWith { + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name Status -Value { + return 'Online' + } -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | + Add-Member -MemberType ScriptMethod -Name Connect -Value { } -PassThru -Force + ) -PassThru -Force + } + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should format the connection string with tcp protocol prefix for default instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -Protocol 'tcp' -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer" + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should format the connection string with tcp protocol prefix for named instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName 'MyInstance' -Protocol 'tcp' -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer\MyInstance" + } + } + + Context 'When connecting using Port parameter' { + BeforeEach { + $mockExpectedDatabaseEngineServer = 'TestServer' + $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + + # Mock that expects port suffix in the ServerInstance + Mock -CommandName New-Object -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter -MockWith { + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name Status -Value { + return 'Online' + } -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | + Add-Member -MemberType ScriptMethod -Name Connect -Value { } -PassThru -Force + ) -PassThru -Force + } + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should format the connection string with port for default instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -Port 1433 -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer,1433" + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should format the connection string with port for named instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName 'MyInstance' -Port 50200 -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "$mockExpectedDatabaseEngineServer\MyInstance,50200" + } + } + + Context 'When connecting using both Protocol and Port parameters' { + BeforeEach { + $mockExpectedDatabaseEngineServer = '192.168.1.1' + $mockExpectedDatabaseEngineInstance = 'MSSQLSERVER' + + # Mock that expects protocol prefix and port suffix in the ServerInstance + Mock -CommandName New-Object -ParameterFilter $mockNewObject_MicrosoftDatabaseEngine_ParameterFilter -MockWith { + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name Status -Value { + return 'Online' + } -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { return $true } -PassThru | + Add-Member -MemberType ScriptMethod -Name Connect -Value { } -PassThru -Force + ) -PassThru -Force + } + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should format the connection string with protocol and port for default instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -Protocol 'tcp' -Port 1433 -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer,1433" + } + + # Skipping on Linux and macOS because they do not support Windows Authentication. + It 'Should format the connection string with protocol and port for named instance' -Skip:($IsLinux -or $IsMacOS) { + $databaseEngineServerObject = Connect-SQL -ServerName $mockExpectedDatabaseEngineServer -InstanceName 'MyInstance' -Protocol 'tcp' -Port 50200 -ErrorAction 'Stop' + $databaseEngineServerObject.ConnectionContext.ServerInstance | Should -BeExactly "tcp:$mockExpectedDatabaseEngineServer\MyInstance,50200" + } + } + + Context 'When connecting to the default instance using the correct service instance but does not return a correct Database Engine object' { + Context 'When using ErrorAction set to Stop' -Skip:($IsLinux -or $IsMacOS) { + BeforeAll { + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' + } -MockWith { + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name Status -Value { + return $null + } -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value 'localhost' -PassThru | + Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name Login -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name SecurePassword -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUser -Value $false -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUserPassword -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUserName -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { + return $true + } -PassThru | + Add-Member -MemberType ScriptMethod -Name Connect -Value { + return + } -PassThru -Force + ) -PassThru -Force + } + } + + It 'Should throw the correct error' { + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.FailedToConnectToDatabaseEngineInstance + } + + $mockErrorMessage = $mockLocalizedString -f 'localhost' + + { Connect-SQL -ServerName 'localhost' -ErrorAction 'Stop' } | + Should -Throw -ExpectedMessage $mockErrorMessage + + Should -Invoke -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When using ErrorAction set to SilentlyContinue' { + BeforeAll { + Mock -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' + } -MockWith { + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name Status -Value { + return $null + } -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectionContext -Value ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name ServerInstance -Value 'localhost' -PassThru | + Add-Member -MemberType NoteProperty -Name LoginSecure -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name Login -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name SecurePassword -Value $null -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUser -Value $false -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUserPassword -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectAsUserName -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name StatementTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ConnectTimeout -Value 600 -PassThru | + Add-Member -MemberType NoteProperty -Name ApplicationName -Value 'SqlServerDsc' -PassThru | + Add-Member -MemberType ScriptMethod -Name Disconnect -Value { + return $true + } -PassThru | + Add-Member -MemberType ScriptMethod -Name Connect -Value { + return + } -PassThru -Force + ) -PassThru -Force + } + } + + It 'Should not throw an exception' { + $null = Connect-SQL -ServerName 'localhost' -SetupCredential $mockSqlCredential -LoginType 'SqlLogin' -ErrorAction 'SilentlyContinue' + + Should -Invoke -CommandName New-Object -ParameterFilter { + $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Server' + } -Exactly -Times 1 -Scope It + } + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Connect-SqlAnalysis.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Connect-SqlAnalysis.Tests.ps1 new file mode 100644 index 000000000..f4e31321b --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Connect-SqlAnalysis.Tests.ps1 @@ -0,0 +1,333 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Connect-SqlAnalysis' -Tag 'ConnectSqlAnalysis' { + BeforeAll { + $mockInstanceName = 'TEST' + $mockDynamicConnectedStatus = $true + + $mockNewObject_MicrosoftAnalysisServicesServer = { + return New-Object -TypeName Object | + Add-Member -MemberType 'NoteProperty' -Name 'Connected' -Value $mockDynamicConnectedStatus -PassThru | + Add-Member -MemberType 'ScriptMethod' -Name 'Connect' -Value { + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DataSource + ) + + if ($DataSource -ne $mockExpectedDataSource) + { + throw ("Datasource was expected to be '{0}', but was '{1}'." -f $mockExpectedDataSource, $dataSource) + } + + if ($mockThrowInvalidOperation) + { + throw 'Unable to connect.' + } + } -PassThru -Force + } + + $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter = { + $TypeName -eq 'Microsoft.AnalysisServices.Server' + } + + $mockSqlCredentialUserName = 'TestUserName12345' + $mockSqlCredentialPassword = 'StrongOne7.' + $mockSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockSqlCredentialPassword -AsPlainText -Force + $mockSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockSqlCredentialUserName, $mockSqlCredentialSecurePassword) + + $mockNetBiosSqlCredentialUserName = 'DOMAIN\TestUserName12345' + $mockNetBiosSqlCredentialPassword = 'StrongOne7.' + $mockNetBiosSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockNetBiosSqlCredentialPassword -AsPlainText -Force + $mockNetBiosSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockNetBiosSqlCredentialUserName, $mockNetBiosSqlCredentialSecurePassword) + + $mockFqdnSqlCredentialUserName = 'TestUserName12345@domain.local' + $mockFqdnSqlCredentialPassword = 'StrongOne7.' + $mockFqdnSqlCredentialSecurePassword = ConvertTo-SecureString -String $mockFqdnSqlCredentialPassword -AsPlainText -Force + $mockFqdnSqlCredential = New-Object -TypeName PSCredential -ArgumentList ($mockFqdnSqlCredentialUserName, $mockFqdnSqlCredentialSecurePassword) + + $mockComputerName = Get-ComputerName + } + + BeforeEach { + Mock -CommandName New-Object ` + -MockWith $mockNewObject_MicrosoftAnalysisServicesServer ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + + Context 'When using feature flag ''AnalysisServicesConnection''' { + BeforeAll { + Mock -CommandName Import-SqlDscPreferredModule + + $mockExpectedDataSource = "Data Source=$mockComputerName" + } + + Context 'When connecting to the default instance using Windows Authentication' { + It 'Should not throw when connecting' { + $null = Connect-SQLAnalysis -FeatureFlag 'AnalysisServicesConnection' + + Should -Invoke -CommandName Import-SqlDscPreferredModule -Exactly -Times 1 -Scope It + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + + Context 'When Connected status is $false' { + BeforeAll { + $mockDynamicConnectedStatus = $false + } + + AfterAll { + $mockDynamicConnectedStatus = $true + } + + It 'Should throw the correct error' { + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.FailedToConnectToAnalysisServicesInstance + } + + $mockErrorRecord = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f $mockComputerName + ) + + { Connect-SQLAnalysis -FeatureFlag 'AnalysisServicesConnection' } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') + } + } + } + + Context 'When connecting to the named instance using Windows Authentication' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName" + + $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -FeatureFlag 'AnalysisServicesConnection' + } + } + + Context 'When connecting to the named instance using Windows Authentication impersonation' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockSqlCredentialUserName;Password=$mockSqlCredentialPassword" + + $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockSqlCredential -FeatureFlag 'AnalysisServicesConnection' + } + } + } + + Context 'When not using feature flag ''AnalysisServicesConnection''' { + BeforeAll { + Mock -CommandName Import-Assembly + } + + Context 'When connecting to the default instance using Windows Authentication' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName" + + $null = Connect-SQLAnalysis + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + + Context 'When connecting to the named instance using Windows Authentication' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName" + + $null = Connect-SQLAnalysis -InstanceName $mockInstanceName + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + + Context 'When connecting to the named instance using Windows Authentication impersonation' { + Context 'When authentication without NetBIOS domain and Fully Qualified Domain Name (FQDN)' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockSqlCredentialUserName;Password=$mockSqlCredentialPassword" + + $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockSqlCredential + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + + Context 'When authentication using NetBIOS domain' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockNetBiosSqlCredentialUserName;Password=$mockNetBiosSqlCredentialPassword" + + $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockNetBiosSqlCredential + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + + Context 'When authentication using Fully Qualified Domain Name (FQDN)' { + It 'Should not throw when connecting' { + $mockExpectedDataSource = "Data Source=$mockComputerName\$mockInstanceName;User ID=$mockFqdnSqlCredentialUserName;Password=$mockFqdnSqlCredentialPassword" + + $null = Connect-SQLAnalysis -InstanceName $mockInstanceName -SetupCredential $mockFqdnSqlCredential + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + } + + Context 'When connecting to the default instance using the correct service instance but does not return a correct Analysis Service object' { + It 'Should throw the correct error' { + $mockExpectedDataSource = '' + + Mock -CommandName New-Object ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.FailedToConnectToAnalysisServicesInstance + } + + $mockErrorRecord = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f $mockComputerName + ) + + { Connect-SQLAnalysis } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + + Context 'When connecting to the default instance using a Analysis Service instance that does not exist' { + It 'Should throw the correct error' { + $mockExpectedDataSource = "Data Source=$mockComputerName" + + # Force the mock of Connect() method to throw 'Unable to connect.' + $mockThrowInvalidOperation = $true + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.FailedToConnectToAnalysisServicesInstance + } + + $mockErrorRecord = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f $mockComputerName + ) + + { Connect-SQLAnalysis } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + + # Setting it back to the default so it does not disturb other tests. + $mockThrowInvalidOperation = $false + } + } + + # This test is to test the mock so that it throws correct when data source is not the expected data source + Context 'When connecting to the named instance using another data source then expected' { + It 'Should throw the correct error' { + $mockExpectedDataSource = 'Force wrong data source' + + $testParameters = @{ + ServerName = 'DummyHost' + InstanceName = $mockInstanceName + } + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.FailedToConnectToAnalysisServicesInstance + } + + $mockErrorRecord = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f "$($testParameters.ServerName)\$($testParameters.InstanceName)" + ) + + { Connect-SQLAnalysis @testParameters } | Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') + + Should -Invoke -CommandName New-Object -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockNewObject_MicrosoftAnalysisServicesServer_ParameterFilter + } + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Connect-UncPath.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Connect-UncPath.Tests.ps1 new file mode 100644 index 000000000..be00a909b --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Connect-UncPath.Tests.ps1 @@ -0,0 +1,212 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Connect-UncPath' -Tag 'ConnectUncPath' { + BeforeAll { + $mockSourcePathUNC = '\\server\share' + + $mockShareCredentialUserName = 'COMPANY\SqlAdmin' + $mockShareCredentialPassword = 'dummyPassW0rd' + $mockShareCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( + $mockShareCredentialUserName, + ($mockShareCredentialPassword | ConvertTo-SecureString -AsPlainText -Force) + ) + + $mockFqdnShareCredentialUserName = 'SqlAdmin@company.local' + $mockFqdnShareCredentialPassword = 'dummyPassW0rd' + $mockFqdnShareCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( + $mockFqdnShareCredentialUserName, + ($mockFqdnShareCredentialPassword | ConvertTo-SecureString -AsPlainText -Force) + ) + + InModuleScope -ScriptBlock { + # Stubs for cross-platform testing. + function script:New-SmbMapping + { + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '', Justification = 'Suppressing this rule because parameter Password is used to mock the real command.')] + [CmdletBinding()] + param + ( + [Parameter()] + [System.String] + $RemotePath, + + [Parameter()] + [System.String] + $UserName, + + [Parameter()] + [System.String] + $Password + ) + + throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand + } + + function script:Remove-SmbMapping + { + throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand + } + } + + Mock -CommandName New-SmbMapping -MockWith { + return @{ + RemotePath = $mockSourcePathUNC + } + } + } + + AfterAll { + InModuleScope -ScriptBlock { + Remove-Item -Path 'function:/New-SmbMapping' + Remove-Item -Path 'function:/Remove-SmbMapping' + } + } + + Context 'When connecting to a UNC path without credentials (using current credentials)' { + It 'Should call the correct mocks' { + $connectUncPathParameters = @{ + RemotePath = $mockSourcePathUNC + } + + $null = Connect-UncPath @connectUncPathParameters -ErrorAction 'Stop' + + Should -Invoke -CommandName New-SmbMapping -ParameterFilter { + <# + Due to issue https://github.com/pester/Pester/issues/1542 + we must use `$null -ne $UserName` instead of + `$PSBoundParameters.ContainsKey('UserName') -eq $false`. + #> + $RemotePath -eq $mockSourcePathUNC ` + -and $null -eq $UserName + } -Exactly -Times 1 -Scope It + } + } + + Context 'When connecting to a UNC path with specific credentials' { + It 'Should call the correct mocks' { + $connectUncPathParameters = @{ + RemotePath = $mockSourcePathUNC + SourceCredential = $mockShareCredential + } + + $null = Connect-UncPath @connectUncPathParameters -ErrorAction 'Stop' + + Should -Invoke -CommandName New-SmbMapping -ParameterFilter { + $RemotePath -eq $mockSourcePathUNC ` + -and $UserName -eq $mockShareCredentialUserName + } -Exactly -Times 1 -Scope It + } + } + + Context 'When connecting using Fully Qualified Domain Name (FQDN)' { + It 'Should call the correct mocks' { + $connectUncPathParameters = @{ + RemotePath = $mockSourcePathUNC + SourceCredential = $mockFqdnShareCredential + } + + $null = Connect-UncPath @connectUncPathParameters -ErrorAction 'Stop' + + Should -Invoke -CommandName New-SmbMapping -ParameterFilter { + $RemotePath -eq $mockSourcePathUNC ` + -and $UserName -eq $mockFqdnShareCredentialUserName + } -Exactly -Times 1 -Scope It + } + } + + Context 'When connecting to a UNC path and using parameter PassThru' { + It 'Should return the correct MSFT_SmbMapping object' { + $connectUncPathParameters = @{ + RemotePath = $mockSourcePathUNC + SourceCredential = $mockShareCredential + PassThru = $true + } + + $connectUncPathResult = Connect-UncPath @connectUncPathParameters + $connectUncPathResult.RemotePath | Should -Be $mockSourcePathUNC + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/ConvertTo-ServerInstanceName.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/ConvertTo-ServerInstanceName.Tests.ps1 new file mode 100644 index 000000000..4cd6f16ab --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/ConvertTo-ServerInstanceName.Tests.ps1 @@ -0,0 +1,102 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\ConvertTo-ServerInstanceName' -Tag 'ConvertToServerInstanceName' { + BeforeAll { + $mockComputerName = Get-ComputerName + } + + It 'Should return correct service instance for a default instance' { + $result = ConvertTo-ServerInstanceName -InstanceName 'MSSQLSERVER' -ServerName $mockComputerName + + $result | Should -BeExactly $mockComputerName + } + + It 'Should return correct service instance for a name instance' { + $result = ConvertTo-ServerInstanceName -InstanceName 'MyInstance' -ServerName $mockComputerName + + $result | Should -BeExactly ('{0}\{1}' -f $mockComputerName, 'MyInstance') + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Copy-ItemWithRobocopy.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Copy-ItemWithRobocopy.Tests.ps1 new file mode 100644 index 000000000..9f451282f --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Copy-ItemWithRobocopy.Tests.ps1 @@ -0,0 +1,393 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +# Tests only the parts of the code that does not already get tested thru the other tests. +Describe 'SqlServerDsc.Common\Copy-ItemWithRobocopy' -Tag 'CopyItemWithRobocopy' { + BeforeAll { + $mockRobocopyExecutableName = 'Robocopy.exe' + $mockRobocopyExecutableVersionWithoutUnbufferedIO = '6.2.9200.00000' + $mockRobocopyExecutableVersionWithUnbufferedIO = '6.3.9600.16384' + $mockRobocopyExecutableVersion = '' # Set dynamically during runtime + $mockRobocopyArgumentSilent = '/njh /njs /ndl /nc /ns /nfl' + $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty = '/e' + $mockRobocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource = '/purge' + $mockRobocopyArgumentUseUnbufferedIO = '/J' + $mockRobocopyArgumentSourcePath = 'C:\Source\SQL2016' + $mockRobocopyArgumentDestinationPath = 'D:\Temp' + $mockRobocopyArgumentSourcePathWithSpaces = 'C:\Source\SQL2016 STD SP1' + $mockRobocopyArgumentDestinationPathWithSpaces = 'D:\Temp\DSC SQL2016' + + $mockGetCommand = { + return @( + ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'Name' -Value $mockRobocopyExecutableName -PassThru | + Add-Member -MemberType ScriptProperty -Name FileVersionInfo -Value { + return @( ( New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'ProductVersion' -Value $mockRobocopyExecutableVersion -PassThru -Force + ) ) + } -PassThru -Force + ) + ) + } + + $mockStartSqlSetupProcessExpectedArgument = '' # Set dynamically during runtime + $mockStartSqlSetupProcessExitCode = 0 # Set dynamically during runtime + + $mockStartSqlSetupProcess_Robocopy = { + if ( $ArgumentList -cne $mockStartSqlSetupProcessExpectedArgument ) + { + throw "Expected arguments was not the same as the arguments in the function call.`nExpected: '$mockStartSqlSetupProcessExpectedArgument' `n But was: '$ArgumentList'" + } + + return New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value 0 -PassThru -Force + } + + $mockStartSqlSetupProcess_Robocopy_WithExitCode = { + return New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value $mockStartSqlSetupProcessExitCode -PassThru -Force + } + } + + Context 'When Copy-ItemWithRobocopy is called it should return the correct arguments' { + BeforeEach { + Mock -CommandName Get-Command -MockWith $mockGetCommand + Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy + $mockRobocopyArgumentSourcePathQuoted = '"{0}"' -f $mockRobocopyArgumentSourcePath + $mockRobocopyArgumentDestinationPathQuoted = '"{0}"' -f $mockRobocopyArgumentDestinationPath + } + + + It 'Should use Unbuffered IO when copying' { + $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO + + $mockStartSqlSetupProcessExpectedArgument = + $mockRobocopyArgumentSourcePathQuoted, + $mockRobocopyArgumentDestinationPathQuoted, + $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty, + $mockRobocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource, + $mockRobocopyArgumentUseUnbufferedIO, + $mockRobocopyArgumentSilent -join ' ' + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + + It 'Should not use Unbuffered IO when copying' { + $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithoutUnbufferedIO + + $mockStartSqlSetupProcessExpectedArgument = + $mockRobocopyArgumentSourcePathQuoted, + $mockRobocopyArgumentDestinationPathQuoted, + $mockRobocopyArgumentCopySubDirectoriesIncludingEmpty, + $mockRobocopyArgumentDeletesDestinationFilesAndDirectoriesNotExistAtSource, + '', + $mockRobocopyArgumentSilent -join ' ' + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + } + + Context 'When Copy-ItemWithRobocopy throws an exception it should return the correct error messages' { + BeforeAll { + $mockRobocopyArgumentSourcePath = 'C:\Source\SQL2016' + $mockRobocopyArgumentDestinationPath = 'D:\Temp\DSCSQL2016' + $mockRobocopyExecutableName = 'Robocopy.exe' + $mockRobocopyExecutableVersion = '' # Set dynamically during runtime + } + + BeforeEach { + $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO + + Mock -CommandName Get-Command -MockWith { + return @( + ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'Name' -Value $mockRobocopyExecutableName -PassThru | + Add-Member -MemberType ScriptProperty -Name FileVersionInfo -Value { + return @( ( New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'ProductVersion' -Value $mockRobocopyExecutableVersion -PassThru -Force + ) ) + } -PassThru -Force + ) + ) + } + + Mock -CommandName Start-Process -MockWith { + return New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'ExitCode' -Value $mockStartSqlSetupProcessExitCode -PassThru -Force + } + } + + It 'Should throw the correct error message when error code is 8' { + $mockStartSqlSetupProcessExitCode = 8 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.RobocopyErrorCopying + } + + $mockErrorMessage = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f $mockStartSqlSetupProcessExitCode + ) + + $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty + + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should -Throw -ExpectedMessage $mockErrorMessage + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + + It 'Should throw the correct error message when error code is 16' { + $mockStartSqlSetupProcessExitCode = 16 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.RobocopyErrorCopying + } + + $mockErrorMessage = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f $mockStartSqlSetupProcessExitCode + ) + + $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty + + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should -Throw -ExpectedMessage $mockErrorMessage + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -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)' { + $mockStartSqlSetupProcessExitCode = 9 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.RobocopyFailuresCopying + } + + $mockErrorMessage = Get-InvalidResultRecord -Message ( + $mockLocalizedString -f $mockStartSqlSetupProcessExitCode + ) + + $mockErrorMessage | Should -Not -BeNullOrEmpty + + { Copy-ItemWithRobocopy @copyItemWithRobocopyParameter } | Should -Throw -ExpectedMessage $mockErrorMessage + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + } + + Context 'When Copy-ItemWithRobocopy is called and finishes successfully it should return the correct exit code' { + BeforeEach { + $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO + + Mock -CommandName Get-Command -MockWith $mockGetCommand + Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy_WithExitCode + } + + AfterEach { + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + + It 'Should finish successfully with exit code 1' { + $mockStartSqlSetupProcessExitCode = 1 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + + It 'Should finish successfully with exit code 2' { + $mockStartSqlSetupProcessExitCode = 2 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + + It 'Should finish successfully with exit code 3' { + $mockStartSqlSetupProcessExitCode = 3 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePath + DestinationPath = $mockRobocopyArgumentDestinationPath + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + } + } + + Context 'When Copy-ItemWithRobocopy is called with spaces in paths and finishes successfully it should return the correct exit code' { + BeforeEach { + $mockRobocopyExecutableVersion = $mockRobocopyExecutableVersionWithUnbufferedIO + + Mock -CommandName Get-Command -MockWith $mockGetCommand + Mock -CommandName Start-Process -MockWith $mockStartSqlSetupProcess_Robocopy_WithExitCode + } + + AfterEach { + Should -Invoke -CommandName Get-Command -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + } + + It 'Should finish successfully with exit code 1' { + $mockStartSqlSetupProcessExitCode = 1 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePathWithSpaces + DestinationPath = $mockRobocopyArgumentDestinationPathWithSpaces + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + } + + It 'Should finish successfully with exit code 2' { + $mockStartSqlSetupProcessExitCode = 2 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePathWithSpaces + DestinationPath = $mockRobocopyArgumentDestinationPathWithSpaces + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + } + + It 'Should finish successfully with exit code 3' { + $mockStartSqlSetupProcessExitCode = 3 + + $copyItemWithRobocopyParameter = @{ + Path = $mockRobocopyArgumentSourcePathWithSpaces + DestinationPath = $mockRobocopyArgumentDestinationPathWithSpaces + } + + $null = Copy-ItemWithRobocopy @copyItemWithRobocopyParameter -ErrorAction 'Stop' + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Disconnect-UncPath.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Disconnect-UncPath.Tests.ps1 new file mode 100644 index 000000000..c8295a65b --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Disconnect-UncPath.Tests.ps1 @@ -0,0 +1,118 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Disconnect-UncPath' -Tag 'DisconnectUncPath' { + BeforeAll { + $mockSourcePathUNC = '\\server\share' + + InModuleScope -ScriptBlock { + # Stubs for cross-platform testing. + function script:Remove-SmbMapping + { + throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand + } + } + + Mock -CommandName Remove-SmbMapping + } + + AfterAll { + InModuleScope -ScriptBlock { + Remove-Item -Path 'function:/Remove-SmbMapping' + } + } + + Context 'When disconnecting from an UNC path' { + It 'Should call the correct mocks' { + $disconnectUncPathParameters = @{ + RemotePath = $mockSourcePathUNC + } + + $null = Disconnect-UncPath @disconnectUncPathParameters + + Should -Invoke -CommandName Remove-SmbMapping -Exactly -Times 1 -Scope It + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Find-ExceptionByNumber.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Find-ExceptionByNumber.Tests.ps1 new file mode 100644 index 000000000..810a240f9 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Find-ExceptionByNumber.Tests.ps1 @@ -0,0 +1,108 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Find-ExceptionByNumber' -Tag 'FindExceptionByNumber' { + BeforeAll { + $mockInnerException = New-Object System.Exception 'This is a mock inner exception object' + $mockInnerException | Add-Member -Name 'Number' -Value 2 -MemberType NoteProperty + + $mockException = New-Object System.Exception 'This is a mock exception object', $mockInnerException + $mockException | Add-Member -Name 'Number' -Value 1 -MemberType NoteProperty + } + + Context 'When searching Exception objects' { + It 'Should return true for main exception' { + Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 1 | Should -BeTrue + } + + It 'Should return true for inner exception' { + Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 2 | Should -BeTrue + } + + It 'Should return false when message not found' { + Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 3 | Should -BeFalse + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Get-PrimaryReplicaServerObject.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Get-PrimaryReplicaServerObject.Tests.ps1 new file mode 100644 index 000000000..01beb98ef --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Get-PrimaryReplicaServerObject.Tests.ps1 @@ -0,0 +1,156 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Get-PrimaryReplicaServerObject' -Tag 'GetPrimaryReplicaServerObject' { + BeforeEach { + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + $mockServerObject.DomainInstanceName = 'Server1' + + $mockAvailabilityGroup = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityGroup + $mockAvailabilityGroup.PrimaryReplicaServerName = 'Server1' + + $mockConnectSql = { + param + ( + [Parameter()] + [System.String] + $ServerName, + + [Parameter()] + [System.String] + $InstanceName + ) + + $mock = @( + ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'DomainInstanceName' -Value $ServerName -PassThru + ) + ) + + # Type the mock as a server object + $mock.PSObject.TypeNames.Insert(0, 'Microsoft.SqlServer.Management.Smo.Server') + + return $mock + } + + Mock -CommandName Connect-SQL -MockWith $mockConnectSql + } + + Context 'When the supplied server object is the primary replica' { + It 'Should return the same server object that was supplied' { + $result = Get-PrimaryReplicaServerObject -ServerObject $mockServerObject -AvailabilityGroup $mockAvailabilityGroup + + $result.DomainInstanceName | Should -Be $mockServerObject.DomainInstanceName + $result.DomainInstanceName | Should -Be $mockAvailabilityGroup.PrimaryReplicaServerName + + Should -Invoke -CommandName Connect-SQL -Scope It -Times 0 -Exactly + } + + It 'Should return the same server object that was supplied when the PrimaryReplicaServerNameProperty is empty' { + $mockAvailabilityGroup.PrimaryReplicaServerName = '' + + $result = Get-PrimaryReplicaServerObject -ServerObject $mockServerObject -AvailabilityGroup $mockAvailabilityGroup + + $result.DomainInstanceName | Should -Be $mockServerObject.DomainInstanceName + $result.DomainInstanceName | Should -Not -Be $mockAvailabilityGroup.PrimaryReplicaServerName + + Should -Invoke -CommandName Connect-SQL -Scope It -Times 0 -Exactly + } + } + + Context 'When the supplied server object is not the primary replica' { + It 'Should the server object of the primary replica' { + $mockAvailabilityGroup.PrimaryReplicaServerName = 'Server2' + + $result = Get-PrimaryReplicaServerObject -ServerObject $mockServerObject -AvailabilityGroup $mockAvailabilityGroup + + $result.DomainInstanceName | Should -Not -Be $mockServerObject.DomainInstanceName + $result.DomainInstanceName | Should -Be $mockAvailabilityGroup.PrimaryReplicaServerName + + Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Get-ServiceAccount.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Get-ServiceAccount.Tests.ps1 new file mode 100644 index 000000000..538d9d9a9 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Get-ServiceAccount.Tests.ps1 @@ -0,0 +1,129 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Get-ServiceAccount' -Tag 'GetServiceAccount' { + BeforeAll { + $mockLocalSystemAccountUserName = 'NT AUTHORITY\SYSTEM' + $mockLocalSystemAccountCredential = New-Object System.Management.Automation.PSCredential $mockLocalSystemAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) + + $mockManagedServiceAccountUserName = 'CONTOSO\msa$' + $mockManagedServiceAccountCredential = New-Object System.Management.Automation.PSCredential $mockManagedServiceAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) + + $mockDomainAccountUserName = 'CONTOSO\User1' + $mockDomainAccountCredential = New-Object System.Management.Automation.PSCredential $mockDomainAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) + + $mockLocalServiceAccountUserName = 'NT SERVICE\MyService' + $mockLocalServiceAccountCredential = New-Object System.Management.Automation.PSCredential $mockLocalServiceAccountUserName, (ConvertTo-SecureString 'Password1' -AsPlainText -Force) + } + + Context 'When getting service account' { + It 'Should return NT AUTHORITY\SYSTEM' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockLocalSystemAccountCredential + + $returnValue.UserName | Should -Be $mockLocalSystemAccountUserName + $returnValue.Password | Should -BeNullOrEmpty + } + + It 'Should return Domain Account and Password' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockDomainAccountCredential + + $returnValue.UserName | Should -Be $mockDomainAccountUserName + $returnValue.Password | Should -Be $mockDomainAccountCredential.GetNetworkCredential().Password + } + + It 'Should return managed service account' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockManagedServiceAccountCredential + + $returnValue.UserName | Should -Be $mockManagedServiceAccountUserName + } + + It 'Should return local service account' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockLocalServiceAccountCredential + + $returnValue.UserName | Should -Be $mockLocalServiceAccountUserName + $returnValue.Password | Should -BeNullOrEmpty + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Get-SqlInstanceMajorVersion.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Get-SqlInstanceMajorVersion.Tests.ps1 new file mode 100644 index 000000000..4d22532f6 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Get-SqlInstanceMajorVersion.Tests.ps1 @@ -0,0 +1,170 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Get-SqlInstanceMajorVersion' -Tag 'GetSqlInstanceMajorVersion' { + BeforeAll { + $mockSqlMajorVersion = 13 + $mockInstanceName = 'TEST' + + $mockGetItemProperty_MicrosoftSQLServer_InstanceNames_SQL = { + return @( + ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name $mockInstanceName -Value $mockInstance_InstanceId -PassThru -Force + ) + ) + } + + $mockGetItemProperty_MicrosoftSQLServer_FullInstanceId_Setup = { + return @( + ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'Version' -Value "$($mockSqlMajorVersion).0.4001.0" -PassThru -Force + ) + ) + } + + $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL = { + $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' + } + + $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup = { + $Path -eq "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$mockInstance_InstanceId\Setup" + } + } + + BeforeEach { + Mock -CommandName Get-ItemProperty ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL ` + -MockWith $mockGetItemProperty_MicrosoftSQLServer_InstanceNames_SQL + + Mock -CommandName Get-ItemProperty ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup ` + -MockWith $mockGetItemProperty_MicrosoftSQLServer_FullInstanceId_Setup + } + + $mockInstance_InstanceId = "MSSQL$($mockSqlMajorVersion).$($mockInstanceName)" + + Context 'When calling Get-SqlInstanceMajorVersion' { + It 'Should return the correct major SQL version number' { + $result = Get-SqlInstanceMajorVersion -InstanceName $mockInstanceName + $result | Should -Be $mockSqlMajorVersion + + Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL + + Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup + } + } + + Context 'When calling Get-SqlInstanceMajorVersion and nothing is returned' { + It 'Should throw the correct error' { + Mock -CommandName Get-ItemProperty ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup ` + -MockWith { + return New-Object -TypeName Object + } + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.SqlServerVersionIsInvalid + } + + $mockErrorMessage = Get-InvalidResultRecord -Message ( + $mockLocalizedString -f $mockInstanceName + ) + + $mockErrorMessage | Should -Not -BeNullOrEmpty + + { Get-SqlInstanceMajorVersion -InstanceName $mockInstanceName } | Should -Throw -ExpectedMessage $mockErrorMessage + + Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_InstanceNames_SQL + + Should -Invoke -CommandName Get-ItemProperty -Exactly -Times 1 -Scope It ` + -ParameterFilter $mockGetItemProperty_ParameterFilter_MicrosoftSQLServer_FullInstanceId_Setup + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Invoke-InstallationMediaCopy.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Invoke-InstallationMediaCopy.Tests.ps1 new file mode 100644 index 000000000..5f67826e0 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Invoke-InstallationMediaCopy.Tests.ps1 @@ -0,0 +1,224 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + + +Describe 'SqlServerDsc.Common\Invoke-InstallationMediaCopy' -Tag 'InvokeInstallationMediaCopy' { + BeforeAll { + $mockSourcePathGuid = 'cc719562-0f46-4a16-8605-9f8a47c70402' + $mockDestinationPath = 'C:\Users\user\AppData\Local\Temp' + + $mockShareCredentialUserName = 'COMPANY\SqlAdmin' + $mockShareCredentialPassword = 'dummyPassW0rd' + $mockShareCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @( + $mockShareCredentialUserName, + ($mockShareCredentialPassword | ConvertTo-SecureString -AsPlainText -Force) + ) + + $mockGetTemporaryFolder = { + return $mockDestinationPath + } + + $mockNewGuid = { + return New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'Guid' -Value $mockSourcePathGuid -PassThru -Force + } + + Mock -CommandName Connect-UncPath + Mock -CommandName Disconnect-UncPath + Mock -CommandName Copy-ItemWithRobocopy + Mock -CommandName Get-TemporaryFolder -MockWith $mockGetTemporaryFolder + Mock -CommandName New-Guid -MockWith $mockNewGuid + } + + Context 'When invoking installation media copy, using SourcePath containing leaf' { + BeforeAll { + $mockSourcePathUNCWithLeaf = '\\server\share\leaf' + + Mock -CommandName Join-Path -MockWith { + return $mockDestinationPath + '\leaf' + } + } + + It 'Should call the correct mocks' { + $invokeInstallationMediaCopyParameters = @{ + SourcePath = $mockSourcePathUNCWithLeaf + SourceCredential = $mockShareCredential + } + + $null = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It + Should -Invoke -CommandName New-Guid -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Get-TemporaryFolder -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Copy-ItemWithRobocopy -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It + } + + It 'Should return the correct destination path' { + $invokeInstallationMediaCopyParameters = @{ + SourcePath = $mockSourcePathUNCWithLeaf + SourceCredential = $mockShareCredential + PassThru = $true + } + + $invokeInstallationMediaCopyResult = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters + + $invokeInstallationMediaCopyResult | Should -Be ('{0}\leaf' -f $mockDestinationPath) + } + } + + Context 'When invoking installation media copy, using SourcePath containing a second leaf' { + BeforeAll { + $mockSourcePathUNCWithLeaf = '\\server\share\leaf\secondleaf' + + Mock -CommandName Join-Path -MockWith { + return $mockDestinationPath + '\secondleaf' + } + } + + It 'Should call the correct mocks' { + $invokeInstallationMediaCopyParameters = @{ + SourcePath = $mockSourcePathUNCWithLeaf + SourceCredential = $mockShareCredential + } + + $null = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It + Should -Invoke -CommandName New-Guid -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Get-TemporaryFolder -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Copy-ItemWithRobocopy -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It + } + + It 'Should return the correct destination path' { + $invokeInstallationMediaCopyParameters = @{ + SourcePath = $mockSourcePathUNCWithLeaf + SourceCredential = $mockShareCredential + PassThru = $true + } + + $invokeInstallationMediaCopyResult = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters + + $invokeInstallationMediaCopyResult | Should -Be ('{0}\secondleaf' -f $mockDestinationPath) + } + } + + Context 'When invoking installation media copy, using SourcePath without a leaf' { + BeforeAll { + $mockSourcePathUNC = '\\server\share' + + Mock -CommandName Join-Path -MockWith { + return $mockDestinationPath + '\' + $mockSourcePathGuid + } + } + + It 'Should call the correct mocks' { + $invokeInstallationMediaCopyParameters = @{ + SourcePath = $mockSourcePathUNC + SourceCredential = $mockShareCredential + } + + $null = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It + Should -Invoke -CommandName New-Guid -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-TemporaryFolder -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Copy-ItemWithRobocopy -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It + } + + It 'Should return the correct destination path' { + $invokeInstallationMediaCopyParameters = @{ + SourcePath = $mockSourcePathUNC + SourceCredential = $mockShareCredential + PassThru = $true + } + + $invokeInstallationMediaCopyResult = Invoke-InstallationMediaCopy @invokeInstallationMediaCopyParameters + $invokeInstallationMediaCopyResult | Should -Be ('{0}\{1}' -f $mockDestinationPath, $mockSourcePathGuid) + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Invoke-SqlScript.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Invoke-SqlScript.Tests.ps1 new file mode 100644 index 000000000..30954b0e8 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Invoke-SqlScript.Tests.ps1 @@ -0,0 +1,281 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Invoke-SqlScript' -Tag 'InvokeSqlScript' { + BeforeAll { + $invokeScriptFileParameters = @{ + ServerInstance = Get-ComputerName + InputFile = 'set.sql' + } + + $invokeScriptQueryParameters = @{ + ServerInstance = Get-ComputerName + Query = 'Test Query' + } + } + + Context 'Invoke-SqlScript fails to import SQLPS module' { + BeforeAll { + $throwMessage = 'Failed to import SQLPS module.' + + Mock -CommandName Import-SqlDscPreferredModule -MockWith { + throw $throwMessage + } + } + + It 'Should throw the correct error from Import-Module' { + { Invoke-SqlScript @invokeScriptFileParameters } | Should -Throw -ExpectedMessage $throwMessage + } + } + + Context 'Invoke-SqlScript is called with credentials' { + BeforeAll { + # Import PowerShell module SqlServer stub cmdlets. + Import-SQLModuleStub + + $mockPasswordPlain = 'password' + $mockUsername = 'User' + + $password = ConvertTo-SecureString -String $mockPasswordPlain -AsPlainText -Force + $credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $mockUsername, $password + + Mock -CommandName Import-SqlDscPreferredModule + Mock -CommandName Invoke-SqlCmd -ParameterFilter { + $Username -eq $mockUsername -and $Password -eq $mockPasswordPlain + } + } + + AfterAll { + # Remove PowerShell module SqlServer stub cmdlets. + Remove-SqlModuleStub + } + + It 'Should call Invoke-SqlCmd with correct File ParameterSet parameters' { + $invokeScriptFileParameters.Add('Credential', $credential) + $null = Invoke-SqlScript @invokeScriptFileParameters + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $Username -eq $mockUsername -and $Password -eq $mockPasswordPlain + } -Times 1 -Exactly -Scope It + } + + It 'Should call Invoke-SqlCmd with correct Query ParameterSet parameters' { + $invokeScriptQueryParameters.Add('Credential', $credential) + $null = Invoke-SqlScript @invokeScriptQueryParameters + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $Username -eq $mockUsername -and $Password -eq $mockPasswordPlain + } -Times 1 -Exactly -Scope It + } + } + + Context 'Invoke-SqlScript fails to execute the SQL scripts' { + BeforeAll { + # Import PowerShell module SqlServer stub cmdlets. + Import-SqlModuleStub + } + + AfterAll { + # Remove PowerShell module SqlServer stub cmdlets. + Remove-SqlModuleStub + } + + BeforeEach { + $errorMessage = 'Failed to run SQL Script' + + Mock -CommandName Import-SqlDscPreferredModule + Mock -CommandName Invoke-SqlCmd -MockWith { + throw $errorMessage + } + } + + It 'Should throw the correct error from File ParameterSet Invoke-SqlCmd' { + { Invoke-SqlScript @invokeScriptFileParameters } | Should -Throw -ExpectedMessage $errorMessage + } + + It 'Should throw the correct error from Query ParameterSet Invoke-SqlCmd' { + { Invoke-SqlScript @invokeScriptQueryParameters } | Should -Throw -ExpectedMessage $errorMessage + } + } + + Context 'Invoke-SqlScript is called with parameter Encrypt' { + BeforeAll { + # Import PowerShell module SqlServer stub cmdlets. + Import-SQLModuleStub + + Mock -CommandName Import-SqlDscPreferredModule + Mock -CommandName Invoke-SqlCmd + } + + AfterAll { + # Remove PowerShell module SqlServer stub cmdlets. + Remove-SqlModuleStub + } + + Context 'When using SqlServer module v22.x' { + BeforeAll { + Mock -CommandName Get-Command -ParameterFilter { + $Name -eq 'Invoke-SqlCmd' + } -MockWith { + return @{ + Parameters = @{ + Keys = @('Encrypt') + } + } + } + } + + It 'Should call Invoke-SqlCmd with correct File ParameterSet parameters' { + $mockInvokeScriptFileParameters = @{ + ServerInstance = Get-ComputerName + InputFile = 'set.sql' + Encrypt = 'Optional' + } + + $null = Invoke-SqlScript @mockInvokeScriptFileParameters + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $Encrypt -eq 'Optional' + } -Times 1 -Exactly -Scope It + } + + It 'Should call Invoke-SqlCmd with correct Query ParameterSet parameters' { + $mockInvokeScriptQueryParameters = @{ + ServerInstance = Get-ComputerName + Query = 'Test Query' + Encrypt = 'Optional' + } + + $null = Invoke-SqlScript @mockInvokeScriptQueryParameters + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $Encrypt -eq 'Optional' + } -Times 1 -Exactly -Scope It + } + } + + Context 'When using SqlServer module v21.x' { + BeforeAll { + Mock -CommandName Get-Command -ParameterFilter { + $Name -eq 'Invoke-SqlCmd' + } -MockWith { + return @{ + Parameters = @{ + Keys = @() + } + } + } + } + + It 'Should call Invoke-SqlCmd with correct File ParameterSet parameters' { + $mockInvokeScriptFileParameters = @{ + ServerInstance = Get-ComputerName + InputFile = 'set.sql' + Encrypt = 'Optional' + } + + $null = Invoke-SqlScript @mockInvokeScriptFileParameters + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $PesterBoundParameters.Keys -notcontains 'Encrypt' + } -Times 1 -Exactly -Scope It + } + + It 'Should call Invoke-SqlCmd with correct Query ParameterSet parameters' { + $mockInvokeScriptQueryParameters = @{ + ServerInstance = Get-ComputerName + Query = 'Test Query' + Encrypt = 'Optional' + } + + $null = Invoke-SqlScript @mockInvokeScriptQueryParameters + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $PesterBoundParameters.Keys -notcontains 'Encrypt' + } -Times 1 -Exactly -Scope It + } + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Restart-SqlClusterService.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Restart-SqlClusterService.Tests.ps1 new file mode 100644 index 000000000..870875b9c --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Restart-SqlClusterService.Tests.ps1 @@ -0,0 +1,498 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +# This test is skipped on Linux and macOS due to it is missing CIM Instance. +Describe 'SqlServerDsc.Common\Restart-SqlClusterService' -Tag 'RestartSqlClusterService' -Skip:($IsLinux -or $IsMacOS) { + Context 'When not clustered instance is found' { + BeforeAll { + Mock -CommandName Get-CimInstance + Mock -CommandName Get-CimAssociatedInstance + Mock -CommandName Invoke-CimMethod + } + + It 'Should not restart any cluster resources' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + } + } + + Context 'When clustered instance is offline' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 3 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance + Mock -CommandName Invoke-CimMethod + } + + It 'Should not restart any cluster resources' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 0 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' + } -Scope It -Exactly -Times 0 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' + } -Scope It -Exactly -Times 0 + } + } + + Context 'When restarting a Sql Server clustered instance' { + Context 'When it is the default instance' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Invoke-CimMethod + } + + It 'Should restart SQL Server cluster resource and the SQL Agent cluster resource' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + } + } + + Context 'When it is a named instance' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (DSCTEST)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'DSCTEST' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Invoke-CimMethod + } + + It 'Should restart SQL Server cluster resource and the SQL Agent cluster resource' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'DSCTEST' -ErrorAction 'Stop' + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (DSCTEST)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (DSCTEST)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (DSCTEST)' + } -Scope It -Exactly -Times 1 + } + } + } + + Context 'When restarting a Sql Server clustered instance and the SQL Agent is offline' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' + # Mock the resource to be offline. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 3 -TypeName 'Int32' + + return $mock + } + + Mock -CommandName Invoke-CimMethod + } + + It 'Should restart the SQL Server cluster resource and ignore the SQL Agent cluster resource online ' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + } + } + + Context 'When passing the parameter OwnerNode' { + Context 'When both the SQL Server and SQL Agent cluster resources is owned by the current node' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' + # Mock the resource to be offline. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' + + return $mock + } + + Mock -CommandName Invoke-CimMethod + } + + It 'Should restart the SQL Server cluster resource and the SQL Agent cluster resource' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + } + } + + Context 'When both the SQL Server and SQL Agent cluster resources is owned by the current node but the SQL Agent cluster resource is offline' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' + # Mock the resource to be offline. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 3 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' + + return $mock + } + + Mock -CommandName Invoke-CimMethod + } + + It 'Should only restart the SQL Server cluster resource' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' + } -Scope It -Exactly -Times 0 + } + } + + Context 'When only the SQL Server cluster resources is owned by the current node' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE1' -TypeName 'String' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value "SQL Server Agent ($($InputObject.PrivateProperties.InstanceName))" -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server Agent' -TypeName 'String' + # Mock the resource to be offline. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE2' -TypeName 'String' + + return $mock + } + + Mock -CommandName Invoke-CimMethod + } + + It 'Should only restart the SQL Server cluster resource' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server (MSSQLSERVER)' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' -and $InputObject.Name -eq 'SQL Server Agent (MSSQLSERVER)' + } -Scope It -Exactly -Times 0 + } + } + + Context 'When the SQL Server cluster resources is not owned by the current node' { + BeforeAll { + Mock -CommandName Get-CimInstance -MockWith { + $mock = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList 'MSCluster_Resource', 'root/MSCluster' + + $mock | Add-Member -MemberType NoteProperty -Name 'Name' -Value 'SQL Server (MSSQLSERVER)' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'Type' -Value 'SQL Server' -TypeName 'String' + $mock | Add-Member -MemberType NoteProperty -Name 'PrivateProperties' -Value @{ + InstanceName = 'MSSQLSERVER' + } + # Mock the resource to be online. + $mock | Add-Member -MemberType NoteProperty -Name 'State' -Value 2 -TypeName 'Int32' + $mock | Add-Member -MemberType NoteProperty -Name 'OwnerNode' -Value 'NODE2' -TypeName 'String' + + return $mock + } + + Mock -CommandName Get-CimAssociatedInstance + Mock -CommandName Invoke-CimMethod + } + + It 'Should not restart any cluster resources' { + InModuleScope -ScriptBlock { + $null = Restart-SqlClusterService -InstanceName 'MSSQLSERVER' -OwnerNode @('NODE1') + } + + Should -Invoke -CommandName Get-CimInstance -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Get-CimAssociatedInstance -Scope It -Exactly -Times 0 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'TakeOffline' + } -Scope It -Exactly -Times 0 + + Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + $MethodName -eq 'BringOnline' + } -Scope It -Exactly -Times 0 + } + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Restart-SqlService.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Restart-SqlService.Tests.ps1 new file mode 100644 index 000000000..992b2d314 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Restart-SqlService.Tests.ps1 @@ -0,0 +1,479 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + + +Describe 'SqlServerDsc.Common\Restart-SqlService' -Tag 'RestartSqlService' { + BeforeAll { + InModuleScope -ScriptBlock { + # Stubs for cross-platform testing. + function script:Get-Service + { + throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand + } + + function script:Restart-Service + { + throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand + } + + function script:Start-Service + { + throw '{0}: StubNotImplemented' -f $MyInvocation.MyCommand + } + } + } + + AfterAll { + InModuleScope -ScriptBlock { + # Remove stubs that was used for cross-platform testing. + Remove-Item -Path function:Get-Service + Remove-Item -Path function:Restart-Service + Remove-Item -Path function:Start-Service + } + } + + Context 'Restart-SqlService standalone instance' { + Context 'When the Windows services should be restarted' { + BeforeAll { + Mock -CommandName Connect-SQL -MockWith { + return @{ + Name = 'MSSQLSERVER' + ServiceName = 'MSSQLSERVER' + Status = 'Online' + IsClustered = $false + } + } + + Mock -CommandName Get-Service -MockWith { + return @{ + Name = 'MSSQLSERVER' + DisplayName = 'Microsoft SQL Server (MSSQLSERVER)' + DependentServices = @( + @{ + Name = 'SQLSERVERAGENT' + DisplayName = 'SQL Server Agent (MSSQLSERVER)' + Status = 'Running' + DependentServices = @() + } + ) + } + } + + Mock -CommandName Restart-Service + Mock -CommandName Start-Service + Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName + } + + It 'Should restart SQL Service and running SQL Agent service' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert just the first call to Connect-SQL. + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. + #> + $ErrorAction -ne 'SilentlyContinue' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 0 -ModuleName $subModuleName + Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 1 + } + + Context 'When skipping the cluster check' { + It 'Should restart SQL Service and running SQL Agent service' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -SkipClusterCheck -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert just the first call to Connect-SQL. + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. + #> + $ErrorAction -ne 'SilentlyContinue' + } -Scope It -Exactly -Times 0 + + Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 0 -ModuleName $subModuleName + Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 1 + } + } + + Context 'When skipping the online check' { + It 'Should restart SQL Service and running SQL Agent service and not wait for the SQL Server instance to come back online' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -SkipWaitForOnline -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert just the first call to Connect-SQL. + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. + #> + $ErrorAction -ne 'SilentlyContinue' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert the second call to Connect-SQL + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $true`. + #> + $ErrorAction -eq 'SilentlyContinue' + } -Scope It -Exactly -Times 0 + + Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 0 -ModuleName $subModuleName + Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 1 + } + } + } + + Context 'When the SQL Server instance is a Failover Cluster instance' { + BeforeAll { + Mock -CommandName Connect-SQL -MockWith { + return @{ + Name = 'MSSQLSERVER' + ServiceName = 'MSSQLSERVER' + Status = 'Online' + IsClustered = $true + } + } + + Mock -CommandName Get-Service + Mock -CommandName Restart-Service + Mock -CommandName Start-Service + Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName + } + + It 'Should just call Restart-SqlClusterService to restart the SQL Server cluster instance' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -ErrorAction 'Stop' + + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert just the first call to Connect-SQL. + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $false`. + #> + $ErrorAction -ne 'SilentlyContinue' + } -Scope It -Exactly -Times 1 + + Should -Invoke -CommandName Restart-SqlClusterService -Scope It -Exactly -Times 1 -ModuleName $subModuleName + Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 0 + Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 0 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 0 + } + + Context 'When passing the Timeout value' { + It 'Should just call Restart-SqlClusterService with the correct parameter' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -Timeout 120 -ErrorAction 'Stop' + + Should -Invoke -CommandName Restart-SqlClusterService -ParameterFilter { + <# + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('Timeout') -eq $true`. + #> + $null -ne $Timeout + } -Scope It -Exactly -Times 1 -ModuleName $subModuleName + } + } + + Context 'When passing the OwnerNode value' { + It 'Should just call Restart-SqlClusterService with the correct parameter' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -OwnerNode @('TestNode') -ErrorAction 'Stop' + + Should -Invoke -CommandName Restart-SqlClusterService -ParameterFilter { + <# + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('OwnerNode') -eq $true`. + #> + $null -ne $OwnerNode + } -Scope It -Exactly -Times 1 -ModuleName $subModuleName + } + } + } + + Context 'When the Windows services should be restarted but there is not SQL Agent service' { + BeforeAll { + Mock -CommandName Connect-SQL -MockWith { + return @{ + Name = 'NOAGENT' + InstanceName = 'NOAGENT' + ServiceName = 'NOAGENT' + Status = 'Online' + } + } + + Mock -CommandName Get-Service -MockWith { + return @{ + Name = 'MSSQL$NOAGENT' + DisplayName = 'Microsoft SQL Server (NOAGENT)' + DependentServices = @() + } + } + + Mock -CommandName Restart-Service + Mock -CommandName Start-Service + Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName + } + + It 'Should restart SQL Service and not try to restart missing SQL Agent service' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'NOAGENT' -SkipClusterCheck -ErrorAction 'Stop' + + Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 0 + } + } + + Context 'When the Windows services should be restarted but the SQL Agent service is stopped' { + BeforeAll { + Mock -CommandName Connect-SQL -MockWith { + return @{ + Name = 'STOPPEDAGENT' + InstanceName = 'STOPPEDAGENT' + ServiceName = 'STOPPEDAGENT' + Status = 'Online' + } + } + + Mock -CommandName Get-Service -MockWith { + return @{ + Name = 'MSSQL$STOPPEDAGENT' + DisplayName = 'Microsoft SQL Server (STOPPEDAGENT)' + DependentServices = @( + @{ + Name = 'SQLAGENT$STOPPEDAGENT' + DisplayName = 'SQL Server Agent (STOPPEDAGENT)' + Status = 'Stopped' + DependentServices = @() + } + ) + } + } + + Mock -CommandName Restart-Service + Mock -CommandName Start-Service + Mock -CommandName Restart-SqlClusterService -ModuleName $subModuleName + } + + It 'Should restart SQL Service and not try to restart stopped SQL Agent service' { + $null = Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'STOPPEDAGENT' -SkipClusterCheck -ErrorAction 'Stop' + + Should -Invoke -CommandName Get-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Restart-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 0 + } + } + + Context 'When it fails to connect to the instance within the timeout period' { + Context 'When the connection throws an exception' { + BeforeAll { + Mock -CommandName Connect-SQL -MockWith { + # Using SilentlyContinue to not show the errors in the Pester output. + Write-Error -Message 'Mock connection error' -ErrorAction 'SilentlyContinue' + } + + Mock -CommandName Get-Service -MockWith { + return @{ + Name = 'MSSQLSERVER' + DisplayName = 'Microsoft SQL Server (MSSQLSERVER)' + DependentServices = @( + @{ + Name = 'SQLSERVERAGENT' + DisplayName = 'SQL Server Agent (MSSQLSERVER)' + Status = 'Running' + DependentServices = @() + } + ) + } + } + + Mock -CommandName Restart-Service + Mock -CommandName Start-Service + } + + It 'Should wait for timeout before throwing error message' { + $mockLocalizedString = InModuleScope -ScriptBlock { + $localizedData.FailedToConnectToInstanceTimeout + } + + $mockErrorMessage = Get-InvalidOperationRecord -Message ( + ($mockLocalizedString -f (Get-ComputerName), 'MSSQLSERVER', 4) + '*Mock connection error*' + ) + + $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty + + { + Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -Timeout 4 -SkipClusterCheck + } | Should -Throw -ExpectedMessage $mockErrorMessage + + <# + Not using -Exactly to handle when CI is slower, result is + that there are 3 calls to Connect-SQL. + #> + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert the second call to Connect-SQL + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $true`. + #> + $ErrorAction -eq 'SilentlyContinue' + } -Scope It -Times 2 + } + } + + Context 'When the Status returns offline' { + BeforeAll { + Mock -CommandName Connect-SQL -MockWith { + return @{ + Name = 'MSSQLSERVER' + InstanceName = '' + ServiceName = 'MSSQLSERVER' + Status = 'Offline' + } + } + + Mock -CommandName Get-Service -MockWith { + return @{ + Name = 'MSSQLSERVER' + DisplayName = 'Microsoft SQL Server (MSSQLSERVER)' + DependentServices = @( + @{ + Name = 'SQLSERVERAGENT' + DisplayName = 'SQL Server Agent (MSSQLSERVER)' + Status = 'Running' + DependentServices = @() + } + ) + } + } + + Mock -CommandName Restart-Service + Mock -CommandName Start-Service + } + + It 'Should wait for timeout before throwing error message' { + $mockLocalizedString = InModuleScope -ScriptBlock { + $localizedData.FailedToConnectToInstanceTimeout + } + + $mockErrorMessage = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f (Get-ComputerName), 'MSSQLSERVER', 4 + ) + + $mockErrorMessage.Exception.Message | Should -Not -BeNullOrEmpty + + { + Restart-SqlService -ServerName (Get-ComputerName) -InstanceName 'MSSQLSERVER' -Timeout 4 -SkipClusterCheck + } | Should -Throw -ExpectedMessage $mockErrorMessage + + <# + Not using -Exactly to handle when CI is slower, result is + that there are 3 calls to Connect-SQL. + #> + Should -Invoke -CommandName Connect-SQL -ParameterFilter { + <# + Make sure we assert the second call to Connect-SQL + + Due to issue https://github.com/pester/Pester/issues/1542 + we cannot use `$PSBoundParameters.ContainsKey('ErrorAction') -eq $true`. + #> + $ErrorAction -eq 'SilentlyContinue' + } -Scope It -Times 2 + } + } + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Split-FullSqlInstanceName.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Split-FullSqlInstanceName.Tests.ps1 new file mode 100644 index 000000000..c668e755c --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Split-FullSqlInstanceName.Tests.ps1 @@ -0,0 +1,114 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Split-FullSqlInstanceName' -Tag 'SplitFullSqlInstanceName' { + Context 'When the "FullSqlInstanceName" parameter is not supplied' { + It 'Should throw when the "FullSqlInstanceName" parameter is $null' { + { Split-FullSqlInstanceName -FullSqlInstanceName $null } | Should -Throw + } + + It 'Should throw when the "FullSqlInstanceName" parameter is an empty string' { + { Split-FullSqlInstanceName -FullSqlInstanceName '' } | Should -Throw + } + } + + Context 'When the "FullSqlInstanceName" parameter is supplied' { + It 'Should throw when the "FullSqlInstanceName" parameter is "ServerName"' { + $result = Split-FullSqlInstanceName -FullSqlInstanceName 'ServerName' + + $result.Count | Should -Be 2 + $result.ServerName | Should -Be 'ServerName' + $result.InstanceName | Should -Be 'MSSQLSERVER' + } + + It 'Should throw when the "FullSqlInstanceName" parameter is "ServerName\InstanceName"' { + $result = Split-FullSqlInstanceName -FullSqlInstanceName 'ServerName\InstanceName' + + $result.Count | Should -Be 2 + $result.ServerName | Should -Be 'ServerName' + $result.InstanceName | Should -Be 'InstanceName' + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Start-SqlSetupProcess.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Start-SqlSetupProcess.Tests.ps1 new file mode 100644 index 000000000..2e5408879 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Start-SqlSetupProcess.Tests.ps1 @@ -0,0 +1,121 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Start-SqlSetupProcess' -Tag 'StartSqlSetupProcess' { + BeforeAll { + $mockPowerShellExecutable = if ($IsLinux -or $IsMacOS) + { + 'pwsh' + } + else + { + 'powershell.exe' + } + } + Context 'When starting a process successfully' { + It 'Should return exit code 0' { + $startSqlSetupProcessParameters = @{ + FilePath = $mockPowerShellExecutable + ArgumentList = '-NonInteractive -NoProfile -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 = $mockPowerShellExecutable + ArgumentList = '-NonInteractive -NoProfile -Command &{Start-Sleep -Seconds 4}' + Timeout = 2 + } + + { Start-SqlSetupProcess @startSqlSetupProcessParameters } | Should -Throw -ErrorId 'ProcessNotTerminated,Microsoft.PowerShell.Commands.WaitProcessCommand' + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Test-ActiveNode.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Test-ActiveNode.Tests.ps1 new file mode 100644 index 000000000..699da5a24 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Test-ActiveNode.Tests.ps1 @@ -0,0 +1,121 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Test-ActiveNode' -Tag 'TestActiveNode' { + BeforeAll { + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + } + + Context 'When function is executed on a standalone instance' { + BeforeAll { + $mockServerObject.IsMemberOfWsfcCluster = $false + } + + It 'Should return $true' { + Test-ActiveNode -ServerObject $mockServerObject | Should -BeTrue + } + } + + Context 'When function is executed on a failover cluster instance (FCI)' { + BeforeAll { + $mockServerObject.IsMemberOfWsfcCluster = $true + } + + It 'Should return when the node name is ' -ForEach @( + @{ + ComputerNamePhysicalNetBIOS = Get-ComputerName + Result = $true + }, + @{ + ComputerNamePhysicalNetBIOS = 'AnotherNode' + Result = $false + } + ) { + $mockServerObject.ComputerNamePhysicalNetBIOS = $ComputerNamePhysicalNetBIOS + + Test-ActiveNode -ServerObject $mockServerObject | Should -Be $Result + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Test-AvailabilityReplicaSeedingModeAutomatic.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Test-AvailabilityReplicaSeedingModeAutomatic.Tests.ps1 new file mode 100644 index 000000000..620182830 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Test-AvailabilityReplicaSeedingModeAutomatic.Tests.ps1 @@ -0,0 +1,164 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Test-AvailabilityReplicaSeedingModeAutomatic' -Tag 'TestAvailabilityReplicaSeedingModeAutomatic' { + BeforeAll { + $mockConnectSql = { + $mock = @( + ( + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'Version' -Value $mockSqlVersion -PassThru + ) + ) + + # Type the mock as a server object + $mock.PSObject.TypeNames.Insert(0, 'Microsoft.SqlServer.Management.Smo.Server') + + return $mock + } + + $mockDynamic_SeedingMode = 'Manual' + $mockInvokeQuery = { + return @{ + Tables = @{ + Rows = @{ + seeding_mode_desc = $mockDynamic_SeedingMode + } + } + } + } + + $testAvailabilityReplicaSeedingModeAutomaticParams = @{ + ServerName = 'Server1' + InstanceName = 'MSSQLSERVER' + AvailabilityGroupName = 'Group1' + AvailabilityReplicaName = 'Replica2' + } + } + + Context 'When the replica seeding mode is manual' { + BeforeEach { + Mock -CommandName Connect-SQL -MockWith $mockConnectSql + Mock -CommandName Invoke-SqlDscQuery -MockWith $mockInvokeQuery + } + + It 'Should return $false when the instance version is <_>' -ForEach @(11, 12) { + $mockSqlVersion = $_ + + Test-AvailabilityReplicaSeedingModeAutomatic @testAvailabilityReplicaSeedingModeAutomaticParams | Should -BeFalse + + Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 0 -Exactly + } + + # Test SQL 2016 and later where Seeding Mode is supported. + It 'Should return $false when the instance version is <_> and the replica seeding mode is manual' -ForEach @(13, 14, 15) { + $mockSqlVersion = $_ + + Test-AvailabilityReplicaSeedingModeAutomatic @testAvailabilityReplicaSeedingModeAutomaticParams | Should -BeFalse + + Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + } + + Context 'When the replica seeding mode is automatic' { + BeforeEach { + Mock -CommandName Connect-SQL -MockWith $mockConnectSql + Mock -CommandName Invoke-SqlDscQuery -MockWith $mockInvokeQuery + } + + # Test SQL 2016 and later where Seeding Mode is supported. + It 'Should return $true when the instance version is <_> and the replica seeding mode is automatic' -ForEach @(13, 14, 15) { + $mockSqlVersion = $_ + $mockDynamic_SeedingMode = 'Automatic' + + Test-AvailabilityReplicaSeedingModeAutomatic @testAvailabilityReplicaSeedingModeAutomaticParams | Should -BeTrue + + Should -Invoke -CommandName Connect-SQL -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Test-ClusterPermissions.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Test-ClusterPermissions.Tests.ps1 new file mode 100644 index 000000000..13309a815 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Test-ClusterPermissions.Tests.ps1 @@ -0,0 +1,173 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Test-ClusterPermissions' -Tag 'TestClusterPermissions' { + BeforeAll { + Mock -CommandName Test-LoginEffectivePermissions -MockWith { + $mockClusterServicePermissionsPresent + } -ParameterFilter { + $LoginName -eq $clusterServiceName + } + + Mock -CommandName Test-LoginEffectivePermissions -MockWith { + $mockSystemPermissionsPresent + } -ParameterFilter { + $LoginName -eq $systemAccountName + } + + $clusterServiceName = 'NT SERVICE\ClusSvc' + $systemAccountName = 'NT AUTHORITY\System' + } + + BeforeEach { + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + $mockServerObject.NetName = 'TestServer' + $mockServerObject.ServiceName = 'MSSQLSERVER' + + $mockLogins = @{ + $clusterServiceName = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Login -ArgumentList $mockServerObject, $clusterServiceName + $systemAccountName = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Login -ArgumentList $mockServerObject, $systemAccountName + } + + $mockServerObject.Logins = $mockLogins + + $mockClusterServicePermissionsPresent = $false + $mockSystemPermissionsPresent = $false + } + + Context 'When the cluster does not have permissions to the instance' { + It "Should throw the correct error when the logins '$($clusterServiceName)' or '$($systemAccountName)' are absent" { + $mockServerObject.Logins = @{} + + { Test-ClusterPermissions -ServerObject $mockServerObject } | Should -Throw -ExpectedMessage ( "The cluster does not have permissions to manage the Availability Group on '{0}\{1}'. Grant 'Connect SQL', 'Alter Any Availability Group', and 'View Server State' to either '$($clusterServiceName)' or '$($systemAccountName)'. (SQLCOMMON0049)" -f $mockServerObject.NetName, $mockServerObject.ServiceName ) + + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 0 -Exactly -ParameterFilter { + $LoginName -eq $clusterServiceName + } + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 0 -Exactly -ParameterFilter { + $LoginName -eq $systemAccountName + } + } + + It "Should throw the correct error when the logins '$($clusterServiceName)' and '$($systemAccountName)' do not have permissions to manage availability groups" { + { Test-ClusterPermissions -ServerObject $mockServerObject } | Should -Throw -ExpectedMessage ( "The cluster does not have permissions to manage the Availability Group on '{0}\{1}'. Grant 'Connect SQL', 'Alter Any Availability Group', and 'View Server State' to either '$($clusterServiceName)' or '$($systemAccountName)'. (SQLCOMMON0049)" -f $mockServerObject.NetName, $mockServerObject.ServiceName ) + + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { + $LoginName -eq $clusterServiceName + } + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { + $LoginName -eq $systemAccountName + } + } + } + + Context 'When the cluster has permissions to the instance' { + It "Should return NullOrEmpty when 'NT SERVICE\ClusSvc' is present and has the permissions to manage availability groups" { + $mockClusterServicePermissionsPresent = $true + + Test-ClusterPermissions -ServerObject $mockServerObject | Should -BeTrue + + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { + $LoginName -eq $clusterServiceName + } + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 0 -Exactly -ParameterFilter { + $LoginName -eq $systemAccountName + } + } + + It "Should return NullOrEmpty when 'NT AUTHORITY\System' is present and has the permissions to manage availability groups" { + $mockSystemPermissionsPresent = $true + + Test-ClusterPermissions -ServerObject $mockServerObject | Should -BeTrue + + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { + $LoginName -eq $clusterServiceName + } + Should -Invoke -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly -ParameterFilter { + $LoginName -eq $systemAccountName + } + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Test-FeatureFlag.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Test-FeatureFlag.Tests.ps1 new file mode 100644 index 000000000..cb09768f5 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Test-FeatureFlag.Tests.ps1 @@ -0,0 +1,104 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'Test-FeatureFlag' -Tag 'TestFeatureFlag' { + Context 'When no feature flags was provided' { + It 'Should return $false' { + Test-FeatureFlag -FeatureFlag $null -TestFlag 'MyFlag' | Should -BeFalse + } + } + + Context 'When feature flags was provided' { + It 'Should return $true' { + Test-FeatureFlag -FeatureFlag @('FirstFlag', 'SecondFlag') -TestFlag 'SecondFlag' | Should -BeTrue + } + } + + Context 'When feature flags was provided, but missing' { + It 'Should return $false' { + Test-FeatureFlag -FeatureFlag @('MyFlag2') -TestFlag 'MyFlag' | Should -BeFalse + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Test-ImpersonatePermissions.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Test-ImpersonatePermissions.Tests.ps1 new file mode 100644 index 000000000..e19b0c6e7 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Test-ImpersonatePermissions.Tests.ps1 @@ -0,0 +1,169 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Test-ImpersonatePermissions' -Tag 'TestImpersonatePermissions' { + BeforeAll { + $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter = { + $Permissions -eq @('IMPERSONATE ANY LOGIN') + } + + $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter = { + $Permissions -eq @('CONTROL SERVER') + } + + $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter = { + $Permissions -eq @('IMPERSONATE') + } + + $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter = { + $Permissions -eq @('CONTROL') + } + + $mockConnectionContextObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ServerConnection + $mockConnectionContextObject.TrueLogin = 'Login1' + + $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server + $mockServerObject.ComputerNamePhysicalNetBIOS = 'Server1' + $mockServerObject.ServiceName = 'MSSQLSERVER' + $mockServerObject.ConnectionContext = $mockConnectionContextObject + } + + BeforeEach { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -MockWith { $false } + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -MockWith { $false } + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -MockWith { $false } + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -MockWith { $false } + } + + Context 'When impersonate permissions are present for the login' { + It 'Should return true when the impersonate any login permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -MockWith { $true } + Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -BeTrue + + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly + } + + It 'Should return true when the control server permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -MockWith { $true } + Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -BeTrue + + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly + } + + It 'Should return true when the impersonate login permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -MockWith { $true } + Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -BeTrue + + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 1 -Exactly + } + + It 'Should return true when the control login permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -MockWith { $true } + Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -BeTrue + + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 1 -Exactly + } + } + + Context 'When impersonate permissions are missing for the login' { + It 'Should return false when the server permissions are missing for the login' { + Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -BeFalse + + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 0 -Exactly + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 0 -Exactly + } + + It 'Should return false when the login permissions are missing for the login' { + Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -BeFalse + + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 1 -Exactly + Should -Invoke -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 1 -Exactly + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Test-LoginEffectivePermissions.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Test-LoginEffectivePermissions.Tests.ps1 new file mode 100644 index 000000000..1194fa833 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Test-LoginEffectivePermissions.Tests.ps1 @@ -0,0 +1,198 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Test-LoginEffectivePermissions' -Tag 'TestLoginEffectivePermissions' { + BeforeAll { + $mockAllServerPermissionsPresent = @( + 'Connect SQL', + 'Alter Any Availability Group', + 'View Server State' + ) + + $mockServerPermissionsMissing = @( + 'Connect SQL', + 'View Server State' + ) + + $mockAllLoginPermissionsPresent = @( + 'View Definition', + 'Impersonate' + ) + + $mockLoginPermissionsMissing = @( + 'View Definition' + ) + + $mockInvokeQueryPermissionsSet = @() # Will be set dynamically in the check + + $mockInvokeQueryPermissionsResult = { + return New-Object -TypeName PSObject -Property @{ + Tables = @{ + Rows = @{ + permission_name = $mockInvokeQueryPermissionsSet + } + } + } + } + + $testLoginEffectiveServerPermissionsParams = @{ + ServerName = 'Server1' + InstanceName = 'MSSQLSERVER' + LoginName = 'NT SERVICE\ClusSvc' + Permissions = @() + } + + $testLoginEffectiveLoginPermissionsParams = @{ + ServerName = 'Server1' + InstanceName = 'MSSQLSERVER' + LoginName = 'NT SERVICE\ClusSvc' + Permissions = @() + SecurableClass = 'LOGIN' + SecurableName = 'Login1' + } + } + + BeforeEach { + Mock -CommandName Invoke-SqlDscQuery -MockWith $mockInvokeQueryPermissionsResult + } + + Context 'When all of the permissions are present' { + It 'Should return $true when the desired server permissions are present' { + $mockInvokeQueryPermissionsSet = $mockAllServerPermissionsPresent.Clone() + $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -BeTrue + + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + + It 'Should return $true when the desired login permissions are present' { + $mockInvokeQueryPermissionsSet = $mockAllLoginPermissionsPresent.Clone() + $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -BeTrue + + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + } + + Context 'When a permission is missing' { + It 'Should return $false when the desired server permissions are not present' { + $mockInvokeQueryPermissionsSet = $mockServerPermissionsMissing.Clone() + $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -BeFalse + + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + + It 'Should return $false when the specified login has no server permissions assigned' { + $mockInvokeQueryPermissionsSet = @() + $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -BeFalse + + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + + It 'Should return $false when the desired login permissions are not present' { + $mockInvokeQueryPermissionsSet = $mockLoginPermissionsMissing.Clone() + $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -BeFalse + + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + + It 'Should return $false when the specified login has no login permissions assigned' { + $mockInvokeQueryPermissionsSet = @() + $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -BeFalse + + Should -Invoke -CommandName Invoke-SqlDscQuery -Scope It -Times 1 -Exactly + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common/Public/Update-AvailabilityGroupReplica.Tests.ps1 b/tests/Unit/SqlServerDsc.Common/Public/Update-AvailabilityGroupReplica.Tests.ps1 new file mode 100644 index 000000000..99565e847 --- /dev/null +++ b/tests/Unit/SqlServerDsc.Common/Public/Update-AvailabilityGroupReplica.Tests.ps1 @@ -0,0 +1,112 @@ +<# + .SYNOPSIS + Unit test for helper functions in module SqlServerDsc.Common. + + .NOTES + SMO stubs + --------- + These are loaded at the start so that it is known that they are left in the + session after test finishes, and will spill over to other tests. There does + not exist a way to unload assemblies. It is possible to load these in a + InModuleScope but the classes are still present in the parent scope when + Pester has ran. + + SqlServer/SQLPS stubs + --------------------- + These are imported using Import-SqlModuleStub in a BeforeAll-block in only + a test that requires them, and must be removed in an AfterAll-block using + Remove-SqlModuleStub so the stub cmdlets does not spill over to another + test. +#> + +# Suppressing this rule because ConvertTo-SecureString is used to simplify the tests. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] +# Suppressing this rule because Script Analyzer does not understand Pester's syntax. +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have 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' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have 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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + $script:subModuleName = 'SqlServerDsc.Common' + + $script:parentModule = Get-Module -Name $script:moduleName -ListAvailable | Select-Object -First 1 + $script:subModulesFolder = Join-Path -Path $script:parentModule.ModuleBase -ChildPath 'Modules' + + $script:subModulePath = Join-Path -Path $script:subModulesFolder -ChildPath $script:subModuleName + + Import-Module -Name $script:subModulePath -ErrorAction 'Stop' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\..\..\TestHelpers\CommonTestHelper.psm1') + + # Loading SMO stubs. + if (-not ('Microsoft.SqlServer.Management.Smo.Server' -as [Type])) + { + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\Stubs') -ChildPath 'SMO.cs') + } + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:subModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:subModuleName +} + +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:subModuleName -All | Remove-Module -Force + + # Remove module common test helper. + Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force +} + +Describe 'SqlServerDsc.Common\Update-AvailabilityGroupReplica' -Tag 'UpdateAvailabilityGroupReplica' { + Context 'When the Availability Group Replica is altered' { + It 'Should silently alter the Availability Group Replica' { + $availabilityReplica = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityReplica + + $null = Update-AvailabilityGroupReplica -AvailabilityGroupReplica $availabilityReplica + } + + It 'Should throw the correct error, AlterAvailabilityGroupReplicaFailed, when altering the Availability Group Replica fails' { + $availabilityReplica = New-Object -TypeName Microsoft.SqlServer.Management.Smo.AvailabilityReplica + $availabilityReplica.Name = 'AlterFailed' + + $mockLocalizedString = InModuleScope -ScriptBlock { + $script:localizedData.AlterAvailabilityGroupReplicaFailed + } + + $mockErrorRecord = Get-InvalidOperationRecord -Message ( + $mockLocalizedString -f $availabilityReplica.Name + ) + + $mockErrorRecord.Exception.Message | Should -Not -BeNullOrEmpty + + { Update-AvailabilityGroupReplica -AvailabilityGroupReplica $availabilityReplica } | + Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') + } + } +}