diff --git a/CHANGELOG.md b/CHANGELOG.md index 136e5e460d..91be06b0f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added comprehensive set of settable database properties that were previously only available in `Set-SqlDscDatabaseProperty` ([issue #2190](https://github.com/dsccommunity/SqlServerDsc/issues/2190)). +- Added public command `Resume-SqlDscDatabase` to bring a database online using + SMO `Database.SetOnline()`. Supports Server and Database pipeline input + ([issue #2191](https://github.com/dsccommunity/SqlServerDsc/issues/2191)). +- Added public command `Suspend-SqlDscDatabase` to take a database offline using + SMO `Database.SetOffline()`. Supports Server and Database pipeline input; + includes `Force` to disconnect active users + ([issue #2192](https://github.com/dsccommunity/SqlServerDsc/issues/2192)). ### Changed diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c73555db99..396264e535 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -341,6 +341,8 @@ stages: 'tests/Integration/Commands/ConvertFrom-SqlDscDatabasePermission.Integration.Tests.ps1' 'tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1' 'tests/Integration/Commands/New-SqlDscDatabaseSnapshot.Integration.Tests.ps1' + 'tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1' + 'tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscCompatibilityLevel.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscDatabaseProperty.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscDatabaseOwner.Integration.Tests.ps1' diff --git a/source/Public/Resume-SqlDscDatabase.ps1 b/source/Public/Resume-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..6323e8f0ed --- /dev/null +++ b/source/Public/Resume-SqlDscDatabase.ps1 @@ -0,0 +1,199 @@ +<# + .SYNOPSIS + Brings a SQL Server database back online. + + .DESCRIPTION + This command brings a SQL Server database back online, making it available + to users again after maintenance or downtime. The command uses the SMO + Database.SetOnline() method to resume the database. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the database to bring online. + + .PARAMETER DatabaseObject + Specifies the database object to bring online (from Get-SqlDscDatabase). + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + trying to get the database object. This is helpful when databases could have been + modified outside of the **ServerObject**, for example through T-SQL. But + on instances with a large amount of databases it might be better to make + sure the **ServerObject** is recent enough. + + This parameter is only used when resuming a database using **ServerObject** and + **Name** parameters. + + .PARAMETER Force + Specifies that the database should be brought online without any confirmation. + + .PARAMETER PassThru + Specifies that the database object should be returned after the operation. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + Resume-SqlDscDatabase -ServerObject $serverObject -Name 'MyDatabase' + + Brings the database named **MyDatabase** back online. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $databaseObject = $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' + Resume-SqlDscDatabase -DatabaseObject $databaseObject -Force + + Brings the database online using a database object without prompting for confirmation. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + Resume-SqlDscDatabase -ServerObject $serverObject -Name 'MyDatabase' -PassThru + + Brings the database online and returns the updated database object. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' | Resume-SqlDscDatabase -Force + + Brings the database online using pipeline input without prompting for confirmation. + + .INPUTS + Microsoft.SqlServer.Management.Smo.Server + + The server object from Connect-SqlDscDatabaseEngine. + + .INPUTS + Microsoft.SqlServer.Management.Smo.Database + + The database object to bring online (from Get-SqlDscDatabase). + + .OUTPUTS + None. + + By default, no output is returned. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.Database + + When PassThru is specified, the updated database object is returned. +#> +function Resume-SqlDscDatabase +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues.')] + [OutputType()] + [OutputType([Microsoft.SqlServer.Management.Smo.Database])] + [CmdletBinding(DefaultParameterSetName = 'ServerObjectSet', SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObjectSet', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'ServerObjectSet', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(ParameterSetName = 'ServerObjectSet')] + [System.Management.Automation.SwitchParameter] + $Refresh, + + [Parameter(ParameterSetName = 'DatabaseObjectSet', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Database] + $DatabaseObject, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru + ) + + begin + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + } + + process + { + # Get the database object based on the parameter set + switch ($PSCmdlet.ParameterSetName) + { + 'ServerObjectSet' + { + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + + $sqlDatabaseObject = $ServerObject | + Get-SqlDscDatabase -Name $Name -Refresh:$Refresh -ErrorAction 'Stop' + + $ErrorActionPreference = $previousErrorActionPreference + } + + 'DatabaseObjectSet' + { + $sqlDatabaseObject = $DatabaseObject + } + } + + $descriptionMessage = $script:localizedData.Database_Resume_ShouldProcessVerboseDescription -f $sqlDatabaseObject.Name, $sqlDatabaseObject.Parent.InstanceName + $confirmationMessage = $script:localizedData.Database_Resume_ShouldProcessVerboseWarning -f $sqlDatabaseObject.Name + $captionMessage = $script:localizedData.Database_Resume_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + <# + Refresh the database object to get the current status if using DatabaseObject + and if Refresh was specified. If ServerObject and Name parameters are used, the + database object is already fresh as Refresh was passed to Get-SqlDscDatabase. + #> + if ($PSCmdlet.ParameterSetName -eq 'DatabaseObjectSet' -and $Refresh.IsPresent) + { + $sqlDatabaseObject.Refresh() + } + + # Check if database has a status other than offline (idempotence) + if (-not $sqlDatabaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) + { + Write-Debug -Message ($script:localizedData.Database_AlreadyOnline -f $sqlDatabaseObject.Name, ($sqlDatabaseObject.Status -join ', ')) + } + else + { + Write-Debug -Message ($script:localizedData.Database_BringingOnline -f $sqlDatabaseObject.Name) + + try + { + $sqlDatabaseObject.SetOnline() + } + catch + { + $errorMessage = $script:localizedData.Database_ResumeFailed -f $sqlDatabaseObject.Name + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($errorMessage, $_.Exception), + 'RSDD0001', # cspell: disable-line + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $sqlDatabaseObject + ) + ) + } + + Write-Debug -Message ($script:localizedData.Database_BroughtOnline -f $sqlDatabaseObject.Name) + } + + if ($PassThru.IsPresent) + { + # Refresh the database object to get the updated Status property + $sqlDatabaseObject.Refresh() + + $sqlDatabaseObject + } + } + } +} diff --git a/source/Public/Suspend-SqlDscDatabase.ps1 b/source/Public/Suspend-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..66f7da173e --- /dev/null +++ b/source/Public/Suspend-SqlDscDatabase.ps1 @@ -0,0 +1,230 @@ +<# + .SYNOPSIS + Takes a SQL Server database offline. + + .DESCRIPTION + This command takes a SQL Server database offline, making it temporarily + unavailable. It is useful for maintenance scenarios, backups, or when you + need to restrict access temporarily. The command uses the SMO + Database.SetOffline() method to suspend the database. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the database to take offline. + + .PARAMETER DatabaseObject + Specifies the database object to take offline (from Get-SqlDscDatabase). + + .PARAMETER Force + Specifies that the database should be forced offline even if users are connected. + When specified, active connections will be disconnected immediately with rollback. + Use this parameter with caution as it can disrupt active sessions. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + trying to get the database object. This is helpful when databases could have been + modified outside of the **ServerObject**, for example through T-SQL. But + on instances with a large amount of databases it might be better to make + sure the **ServerObject** is recent enough. + + This parameter is only used when suspending a database using **ServerObject** and + **Name** parameters. + + .PARAMETER PassThru + Specifies that the database object should be returned after the operation. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + Suspend-SqlDscDatabase -ServerObject $serverObject -Name 'MyDatabase' + + Takes the database named **MyDatabase** offline. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $databaseObject = $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' + Suspend-SqlDscDatabase -DatabaseObject $databaseObject -Force + + Takes the database offline using a database object, forcing disconnection of any active users. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + Suspend-SqlDscDatabase -ServerObject $serverObject -Name 'MyDatabase' -PassThru + + Takes the database offline and returns the updated database object. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' | Suspend-SqlDscDatabase -Force + + Takes the database offline using pipeline input, forcing disconnection of any active users. + + .INPUTS + Microsoft.SqlServer.Management.Smo.Server + + The server object from Connect-SqlDscDatabaseEngine. + + .INPUTS + Microsoft.SqlServer.Management.Smo.Database + + The database object to take offline (from Get-SqlDscDatabase). + + .OUTPUTS + None. + + By default, no output is returned. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.Database + + When PassThru is specified, the updated database object is returned. +#> +function Suspend-SqlDscDatabase +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues.')] + [OutputType()] + [OutputType([Microsoft.SqlServer.Management.Smo.Database])] + [CmdletBinding(DefaultParameterSetName = 'ServerObjectSet', SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObjectSet', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'ServerObjectSet', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObjectSet')] + [System.Management.Automation.SwitchParameter] + $Refresh, + + [Parameter(ParameterSetName = 'DatabaseObjectSet', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Database] + $DatabaseObject, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru + ) + + begin + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + } + + process + { + # Get the database object based on the parameter set + switch ($PSCmdlet.ParameterSetName) + { + 'ServerObjectSet' + { + $previousErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Stop' + + $sqlDatabaseObject = $ServerObject | + Get-SqlDscDatabase -Name $Name -Refresh:$Refresh -ErrorAction 'Stop' + + $ErrorActionPreference = $previousErrorActionPreference + } + + 'DatabaseObjectSet' + { + $sqlDatabaseObject = $DatabaseObject + } + } + + $descriptionMessage = $script:localizedData.Database_Suspend_ShouldProcessVerboseDescription -f $sqlDatabaseObject.Name, $sqlDatabaseObject.Parent.InstanceName + $confirmationMessage = $script:localizedData.Database_Suspend_ShouldProcessVerboseWarning -f $sqlDatabaseObject.Name + $captionMessage = $script:localizedData.Database_Suspend_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + <# + Refresh the database object to get the current status if using DatabaseObject + and if Refresh was specified. If ServerObject and Name parameters are used, the + database object is already fresh as Refresh was passed to Get-SqlDscDatabase. + #> + if ($PSCmdlet.ParameterSetName -eq 'DatabaseObjectSet' -and $Refresh.IsPresent) + { + $sqlDatabaseObject.Refresh() + } + + # Check if database is already offline (idempotence) + if ($sqlDatabaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) + { + Write-Debug -Message ($script:localizedData.Database_AlreadyOffline -f $sqlDatabaseObject.Name, ($sqlDatabaseObject.Status -join ', ')) + } + else + { + if ($Force.IsPresent) + { + Write-Debug -Message ($script:localizedData.Database_TakingOfflineWithForce -f $sqlDatabaseObject.Name) + + # Kill all processes before taking the database offline + Write-Debug -Message ($script:localizedData.Database_KillingProcesses -f $sqlDatabaseObject.Name) + + try + { + $sqlDatabaseObject.Parent.KillAllProcesses($sqlDatabaseObject.Name) + } + catch + { + $errorMessage = $script:localizedData.Database_KillProcessesFailed -f $sqlDatabaseObject.Name + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($errorMessage, $_.Exception), + 'SSDD0002', # cspell: disable-line + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $sqlDatabaseObject + ) + ) + } + } + else + { + Write-Debug -Message ($script:localizedData.Database_TakingOffline -f $sqlDatabaseObject.Name) + } + + try + { + $sqlDatabaseObject.SetOffline() + } + catch + { + $errorMessage = $script:localizedData.Database_SuspendFailed -f $sqlDatabaseObject.Name + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($errorMessage, $_.Exception), + 'SSDD0001', # cspell: disable-line + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $sqlDatabaseObject + ) + ) + } + + Write-Debug -Message ($script:localizedData.Database_TakenOffline -f $sqlDatabaseObject.Name) + } + + if ($PassThru.IsPresent) + { + # Refresh the database object to get the updated Status property + $sqlDatabaseObject.Refresh() + + $sqlDatabaseObject + } + } + } +} diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 702c027cc7..442d260e23 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -366,6 +366,29 @@ ConvertFrom-StringData @' # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. Database_Create_ShouldProcessCaption = Create database on instance + ## Resume-SqlDscDatabase + Database_Resume_ShouldProcessVerboseDescription = Bringing the database '{0}' online on the instance '{1}'. + Database_Resume_ShouldProcessVerboseWarning = Are you sure you want to bring the database '{0}' online? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Database_Resume_ShouldProcessCaption = Bring database online + Database_AlreadyOnline = Database '{0}' is already online (Status: {1}). + Database_BringingOnline = Bringing database '{0}' online. + Database_BroughtOnline = Database '{0}' was brought online successfully. + Database_ResumeFailed = Failed to bring database '{0}' online. + + ## Suspend-SqlDscDatabase + Database_Suspend_ShouldProcessVerboseDescription = Taking the database '{0}' offline on the instance '{1}'. + Database_Suspend_ShouldProcessVerboseWarning = Are you sure you want to take the database '{0}' offline? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Database_Suspend_ShouldProcessCaption = Take database offline + Database_AlreadyOffline = Database '{0}' is already offline (Status: {1}). + Database_TakingOffline = Taking database '{0}' offline. + Database_TakingOfflineWithForce = Taking database '{0}' offline with force (disconnecting active users). + Database_KillingProcesses = Killing all processes for database '{0}'. + Database_TakenOffline = Database '{0}' was taken offline successfully. + Database_SuspendFailed = Failed to take database '{0}' offline. + Database_KillProcessesFailed = Failed to kill processes for database '{0}'. + ## New-SqlDscDatabaseSnapshot DatabaseSnapshot_Create = Creating database snapshot '{0}' from source database '{1}' on instance '{2}'. (NSDS0002) DatabaseSnapshot_EditionNotSupported = Database snapshots are not supported on SQL Server instance '{0}' with edition '{1}'. Snapshots are only supported in Enterprise, Developer, and Evaluation editions. (NSDS0001) diff --git a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..fcf0cfa2b9 --- /dev/null +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,172 @@ +[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' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' +} + +Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' + $mockSqlAdministratorPassword = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force + + $script:mockSqlAdminCredential = [System.Management.Automation.PSCredential]::new($mockSqlAdministratorUserName, $mockSqlAdministratorPassword) + + $script:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:mockInstanceName -Credential $script:mockSqlAdminCredential -ErrorAction 'Stop' + + # Test database names + $script:testDatabaseName = 'SqlDscTestResumeDb_' + (Get-Random) + $script:testDatabaseNameForObject = 'SqlDscTestResumeDbObj_' + (Get-Random) + + # Create test databases + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Force -ErrorAction 'Stop' + + Write-Verbose -Message "Created test databases '$($script:testDatabaseName)' and '$($script:testDatabaseNameForObject)'." -Verbose + } + + AfterAll { + # Clean up test databases + $testDatabasesToRemove = @($script:testDatabaseName, $script:testDatabaseNameForObject) + + foreach ($dbName in $testDatabasesToRemove) + { + $existingDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $dbName -ErrorAction 'SilentlyContinue' + + if ($existingDb) + { + # Ensure database is online before removing + if ($existingDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) + { + $null = Resume-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'SilentlyContinue' + } + + $null = Remove-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'Stop' + } + } + + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject -ErrorAction 'Stop' + } + + Context 'When bringing a database online using ServerObject parameter set' { + It 'Should bring the database online successfully' { + # First take the database offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Verify database is offline + $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + + # Bring the database online + $resultDb = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + + # Verify the change + $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + } + + It 'Should be idempotent when database is already online' { + # Ensure database is online + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Bring online again - should not throw + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Verify the database is still online + $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + } + + It 'Should throw error when trying to bring online non-existent database' { + { Resume-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction 'Stop' } | + Should -Throw + } + } + + Context 'When bringing a database online using DatabaseObject parameter set' { + It 'Should bring the database online using DatabaseObject' { + # First take the database offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Force -ErrorAction 'Stop' + + # Get the database object + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $databaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + + # Bring the database online + $resultDb = Resume-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + + # Verify the change + $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + } + } + + Context 'When using pipeline input' { + It 'Should bring the database online via pipeline using ServerObject' { + # First take the database offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -Force -ErrorAction 'Stop' + + # Bring online via pipeline + $resultDb = $script:serverObject | Resume-SqlDscDatabase -Name $script:testDatabaseName -Force -Refresh -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + } + + It 'Should bring the database online via pipeline using DatabaseObject' { + # First take the database offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -Force -ErrorAction 'Stop' + + # Get the database object and bring online via pipeline + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $resultDb = $databaseObject | Resume-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + } + } + + Context 'When using with Get-SqlDscDatabase' { + It 'Should bring the database online using Get-SqlDscDatabase piped to Resume-SqlDscDatabase' { + # First take the database offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Bring online using pipeline + $resultDb = $script:serverObject | Get-SqlDscDatabase -Name $script:testDatabaseName -Refresh | Resume-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + } + } +} diff --git a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..3907e7abd9 --- /dev/null +++ b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,166 @@ +[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' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' +} + +Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' + $mockSqlAdministratorPassword = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force + + $script:mockSqlAdminCredential = [System.Management.Automation.PSCredential]::new($mockSqlAdministratorUserName, $mockSqlAdministratorPassword) + + $script:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:mockInstanceName -Credential $script:mockSqlAdminCredential -ErrorAction 'Stop' + + # Test database names + $script:testDatabaseName = 'SqlDscTestSuspendDb_' + (Get-Random) + $script:testDatabaseNameForObject = 'SqlDscTestSuspendDbObj_' + (Get-Random) + + # Create test databases + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Force -ErrorAction 'Stop' + + Write-Verbose -Message "Created test databases '$($script:testDatabaseName)' and '$($script:testDatabaseNameForObject)'." -Verbose + } + + AfterAll { + # Clean up test databases + $testDatabasesToRemove = @($script:testDatabaseName, $script:testDatabaseNameForObject) + + foreach ($dbName in $testDatabasesToRemove) + { + $existingDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $dbName -ErrorAction 'SilentlyContinue' + + if ($existingDb) + { + # Ensure database is online before removing + if ($existingDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) + { + $null = Resume-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'SilentlyContinue' + } + + $null = Remove-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'Stop' + } + } + + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject -ErrorAction 'Stop' + } + + Context 'When taking a database offline using ServerObject parameter set' { + It 'Should take the database offline successfully' { + # Ensure database is online + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Verify database is online + $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + + # Take the database offline + $resultDb = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + + # Verify the change + $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + } + + It 'Should be idempotent when database is already offline' { + # Ensure database is offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Take offline again - should not throw + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Verify the database is still offline + $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + } + + It 'Should throw error when trying to take offline non-existent database' { + { Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction 'Stop' } | + Should -Throw + } + } + + Context 'When taking a database offline using DatabaseObject parameter set' { + It 'Should take the database offline using DatabaseObject' { + # Ensure database is online + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Force -ErrorAction 'Stop' + + # Get the database object + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $databaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $databaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue + + # Take the database offline + $resultDb = Suspend-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + + # Verify the change + $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + } + } + + Context 'When using pipeline input' { + It 'Should take the database offline via pipeline using ServerObject' { + # Ensure database is online + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Take offline via pipeline + $resultDb = $script:serverObject | Suspend-SqlDscDatabase -Name $script:testDatabaseName -Force -Refresh -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + } + + It 'Should take the database offline via pipeline using DatabaseObject' { + # Ensure database is online + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Force -ErrorAction 'Stop' + + # Get the database object and take offline via pipeline + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $resultDb = $databaseObject | Suspend-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + } + } + + Context 'When using with Get-SqlDscDatabase' { + It 'Should take the database offline using Get-SqlDscDatabase piped to Suspend-SqlDscDatabase' { + # Ensure database is online + $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' + + # Take offline using pipeline + $resultDb = $script:serverObject | Get-SqlDscDatabase -Name $script:testDatabaseName -Refresh | Suspend-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue + } + } +} diff --git a/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..2592afd585 --- /dev/null +++ b/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,260 @@ +<# + .SYNOPSIS + Unit tests for Resume-SqlDscDatabase. + + .DESCRIPTION + Unit tests for Resume-SqlDscDatabase. +#> + +[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:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Resume-SqlDscDatabase' -Tag 'Public' { + Context 'When the command has proper parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'ServerObjectSet' + ExpectedParameters = '-ServerObject -Name [-Refresh] [-Force] [-PassThru] [-WhatIf] [-Confirm] []' + } + @{ + ExpectedParameterSetName = 'DatabaseObjectSet' + ExpectedParameters = '-DatabaseObject [-Force] [-PassThru] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Resume-SqlDscDatabase').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + } + + Context 'When verifying parameter properties' { + BeforeAll { + $command = Get-Command -Name 'Resume-SqlDscDatabase' + } + + It 'Should have ServerObject as a mandatory parameter in ServerObjectSet' { + $parameterInfo = $command.Parameters['ServerObject'] + + $parameterInfo.ParameterSets['ServerObjectSet'].IsMandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter in ServerObjectSet' { + $parameterInfo = $command.Parameters['Name'] + + $parameterInfo.ParameterSets['ServerObjectSet'].IsMandatory | Should -BeTrue + } + + It 'Should have DatabaseObject as a mandatory parameter in DatabaseObjectSet' { + $parameterInfo = $command.Parameters['DatabaseObject'] + + $parameterInfo.ParameterSets['DatabaseObjectSet'].IsMandatory | Should -BeTrue + } + } + + Context 'When bringing a database online using ServerObject and Name' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should bring the database online and not throw' { + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + + Should -Invoke -CommandName 'Get-SqlDscDatabase' -Exactly -Times 1 -Scope It + } + + It 'Should call Get-SqlDscDatabase with Refresh when specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force + + Should -Invoke -CommandName 'Get-SqlDscDatabase' -ParameterFilter { + $Refresh -eq $true + } -Exactly -Times 1 -Scope It + } + + It 'Should return the database object when PassThru is specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + $result = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -PassThru -Force + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.Name | Should -Be 'TestDatabase' + } + + It 'Should not bring the database online when database is already online' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } + + Context 'When bringing a database online using DatabaseObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + } + + It 'Should bring the database online using DatabaseObject parameter' { + $null = Resume-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + + It 'Should return the database object when PassThru is specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + $result = Resume-SqlDscDatabase -DatabaseObject $mockDatabaseObject -PassThru -Force + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.Name | Should -Be 'TestDatabase' + } + } + + Context 'When bringing a database online via pipeline using ServerObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should bring the database online via pipeline' { + $null = $mockServerObject | Resume-SqlDscDatabase -Name 'TestDatabase' -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } + + Context 'When bringing a database online via pipeline using DatabaseObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + } + + It 'Should bring the database online via pipeline' { + $null = $mockDatabaseObject | Resume-SqlDscDatabase -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } + + Context 'When WhatIf is used' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should not bring the database online when WhatIf is specified' { + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } + + Context 'When an error occurs during the operation' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + # Override SetOnline to throw an exception + $mockDatabaseObject | Add-Member -MemberType ScriptMethod -Name SetOnline -Value { + throw 'Failed to bring database online' + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should throw a terminating error when SetOnline fails' { + { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Throw -ExpectedMessage '*Failed to bring database*online*' + } + } +} diff --git a/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..814e5abf62 --- /dev/null +++ b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,309 @@ +<# + .SYNOPSIS + Unit tests for Suspend-SqlDscDatabase. + + .DESCRIPTION + Unit tests for Suspend-SqlDscDatabase. +#> + +[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:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { + Context 'When the command has proper parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'ServerObjectSet' + ExpectedParameters = '-ServerObject -Name [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + @{ + ExpectedParameterSetName = 'DatabaseObjectSet' + ExpectedParameters = '-DatabaseObject [-Force] [-PassThru] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Suspend-SqlDscDatabase').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + } + + Context 'When verifying parameter properties' { + BeforeAll { + $command = Get-Command -Name 'Suspend-SqlDscDatabase' + } + + It 'Should have ServerObject as a mandatory parameter in ServerObjectSet' { + $parameterInfo = $command.Parameters['ServerObject'] + + $parameterInfo.ParameterSets['ServerObjectSet'].IsMandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter in ServerObjectSet' { + $parameterInfo = $command.Parameters['Name'] + + $parameterInfo.ParameterSets['ServerObjectSet'].IsMandatory | Should -BeTrue + } + + It 'Should have DatabaseObject as a mandatory parameter in DatabaseObjectSet' { + $parameterInfo = $command.Parameters['DatabaseObject'] + + $parameterInfo.ParameterSets['DatabaseObjectSet'].IsMandatory | Should -BeTrue + } + } + + Context 'When taking a database offline using ServerObject and Name' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should take the database offline and not throw' { + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + + Should -Invoke -CommandName 'Get-SqlDscDatabase' -Exactly -Times 1 -Scope It + } + + It 'Should call Get-SqlDscDatabase with Refresh when specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force + + Should -Invoke -CommandName 'Get-SqlDscDatabase' -ParameterFilter { + $Refresh -eq $true + } -Exactly -Times 1 -Scope It + } + + It 'Should return the database object when PassThru is specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + $result = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -PassThru -Force + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.Name | Should -Be 'TestDatabase' + } + + It 'Should not take the database offline when database is already offline' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } + + Context 'When taking a database offline using DatabaseObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + } + + It 'Should take the database offline using DatabaseObject parameter' { + $null = Suspend-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + + It 'Should return the database object when PassThru is specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + $result = Suspend-SqlDscDatabase -DatabaseObject $mockDatabaseObject -PassThru -Force + + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.Name | Should -Be 'TestDatabase' + } + } + + Context 'When taking a database offline via pipeline using ServerObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should take the database offline via pipeline' { + $null = $mockServerObject | Suspend-SqlDscDatabase -Name 'TestDatabase' -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } + + Context 'When taking a database offline via pipeline using DatabaseObject' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + } + + It 'Should take the database offline via pipeline' { + $null = $mockDatabaseObject | Suspend-SqlDscDatabase -Force + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } + + Context 'When using Force parameter to disconnect active users' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + $script:killAllProcessesCalled = $false + + # Override KillAllProcesses to track if it was called + $mockServerObject | Add-Member -MemberType ScriptMethod -Name KillAllProcesses -Value { + param($DatabaseName) + + $script:killAllProcessesCalled = $true + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should call KillAllProcesses when Force parameter is specified' { + $script:killAllProcessesCalled = $false + + Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force -Confirm:$false + + $script:killAllProcessesCalled | Should -BeTrue + } + + It 'Should not call KillAllProcesses when Force parameter is not specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + $script:killAllProcessesCalled = $false + + Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Confirm:$false + + $script:killAllProcessesCalled | Should -BeFalse + } + } + + Context 'When WhatIf is used' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should not take the database offline when WhatIf is specified' { + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf + + $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } + + Context 'When an error occurs during the operation' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + # Override SetOffline to throw an exception + $mockDatabaseObject | Add-Member -MemberType ScriptMethod -Name SetOffline -Value { + throw 'Failed to take database offline' + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should throw a terminating error when SetOffline fails' { + { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Confirm:$false } | Should -Throw -ExpectedMessage '*Failed to take database*offline*' + } + + It 'Should throw a terminating error when KillAllProcesses fails' { + # Override KillAllProcesses to throw an exception + $mockServerObject | Add-Member -MemberType ScriptMethod -Name KillAllProcesses -Value { + throw 'Failed to kill processes' + } -Force + + { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force -Confirm:$false } | Should -Throw -ExpectedMessage '*Failed to kill processes*' + } + } +} diff --git a/tests/Unit/Stubs/SMO.cs b/tests/Unit/Stubs/SMO.cs index b124d67ad5..5294e8fc77 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -524,6 +524,10 @@ public void Revoke( Microsoft.SqlServer.Management.Smo.ServerPermissionSet permi { } + public void KillAllProcesses( string databaseName ) + { + } + // Property for SQL Agent support public Microsoft.SqlServer.Management.Smo.Agent.JobServer JobServer { get; set; } @@ -986,6 +990,16 @@ public void SetSnapshotIsolation( bool enable ) this.SnapshotIsolationState = SnapshotIsolationState.Disabled; } } + + public void SetOnline() + { + this.Status = DatabaseStatus.Normal; + } + + public void SetOffline() + { + this.Status = DatabaseStatus.Offline; + } } // TypeName: Microsoft.SqlServer.Management.Smo.FileGroup