From a76cf46d43a26017762b9b9569749bb7c172f116 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 16:45:00 +0100 Subject: [PATCH 01/14] `Suspend-SqlDscDatabase`/`Resume-SqlDscDatabase`: Command proposals --- CHANGELOG.md | 9 + source/Public/Resume-SqlDscDatabase.ps1 | 189 +++++++++++ source/Public/Suspend-SqlDscDatabase.ps1 | 199 ++++++++++++ source/en-US/SqlServerDsc.strings.psd1 | 21 ++ ...esume-SqlDscDatabase.Integration.Tests.ps1 | 164 ++++++++++ ...spend-SqlDscDatabase.Integration.Tests.ps1 | 164 ++++++++++ .../Public/Resume-SqlDscDatabase.Tests.ps1 | 260 +++++++++++++++ .../Public/Suspend-SqlDscDatabase.Tests.ps1 | 298 ++++++++++++++++++ tests/Unit/Stubs/SMO.cs | 10 + 9 files changed, 1314 insertions(+) create mode 100644 source/Public/Resume-SqlDscDatabase.ps1 create mode 100644 source/Public/Suspend-SqlDscDatabase.ps1 create mode 100644 tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 create mode 100644 tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 create mode 100644 tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 create mode 100644 tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a3bd82b0..b5f2807c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added public command `Resume-SqlDscDatabase` to bring a SQL Server database back + online. This command uses the SMO `Database.SetOnline()` method to make databases + available after maintenance or downtime. The command supports both Server and + Database object pipeline input for flexible usage ([issue #2191](https://github.com/dsccommunity/SqlServerDsc/issues/2191)). +- Added public command `Suspend-SqlDscDatabase` to take a SQL Server database offline. + This command uses the SMO `Database.SetOffline()` method to temporarily make + databases unavailable for maintenance scenarios. The command includes a `Force` + parameter to disconnect active users when necessary and supports both Server and + Database object pipeline input ([issue #2192](https://github.com/dsccommunity/SqlServerDsc/issues/2192)). - Added public command `Enable-SqlDscDatabaseSnapshotIsolation` to enable snapshot isolation for a database in a SQL Server Database Engine instance. This command uses the SMO `SetSnapshotIsolation()` method to enable row-versioning and snapshot diff --git a/source/Public/Resume-SqlDscDatabase.ps1 b/source/Public/Resume-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..fd2e0de882 --- /dev/null +++ b/source/Public/Resume-SqlDscDatabase.ps1 @@ -0,0 +1,189 @@ +<# + .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)) + { + # Check if database is already online (idempotence) + if ($sqlDatabaseObject.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + { + Write-Verbose -Message ($script:localizedData.Database_AlreadyOnline -f $sqlDatabaseObject.Name) + } + else + { + Write-Verbose -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-Verbose -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..f28148f914 --- /dev/null +++ b/source/Public/Suspend-SqlDscDatabase.ps1 @@ -0,0 +1,199 @@ +<# + .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)) + { + # Check if database is already offline (idempotence) + if ($sqlDatabaseObject.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + { + Write-Verbose -Message ($script:localizedData.Database_AlreadyOffline -f $sqlDatabaseObject.Name) + } + else + { + if ($Force.IsPresent) + { + Write-Verbose -Message ($script:localizedData.Database_TakingOfflineWithForce -f $sqlDatabaseObject.Name) + } + else + { + Write-Verbose -Message ($script:localizedData.Database_TakingOffline -f $sqlDatabaseObject.Name) + } + + try + { + $sqlDatabaseObject.SetOffline($Force.IsPresent) + } + 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-Verbose -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..9e5274b486 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -366,6 +366,27 @@ 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. + 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. + Database_TakingOffline = Taking database '{0}' offline. + Database_TakingOfflineWithForce = Taking database '{0}' offline with force (disconnecting active users). + Database_TakenOffline = Database '{0}' was taken offline successfully. + Database_SuspendFailed = Failed to take database '{0}' offline. + ## 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..52c4588359 --- /dev/null +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,164 @@ +[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 -eq [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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + + # Bring the database online + $resultDb = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + + # Verify the change + $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $onlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + + 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + + It 'Should throw error when trying to bring online non-existent database' { + { Resume-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction 'Stop' } | + Should -Throw -ExpectedMessage '*Database*NonExistentDatabase*not found*' + } + } + + 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + + # Bring the database online + $resultDb = Resume-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + + # Verify the change + $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $onlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } + + 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 -Force -ErrorAction 'Stop' + + # Bring online via pipeline + $resultDb = $script:serverObject | Resume-SqlDscDatabase -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + + It 'Should bring the database online via pipeline using DatabaseObject' { + # First take the database offline + $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } + + 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 | Resume-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + } + } +} 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..033432e48c --- /dev/null +++ b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,164 @@ +[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 -eq [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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + + # Take the database offline + $resultDb = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + + # Verify the change + $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' + $offlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + + 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + + It 'Should throw error when trying to take offline non-existent database' { + { Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction 'Stop' } | + Should -Throw -ExpectedMessage '*Database*NonExistentDatabase*not found*' + } + } + + 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + + # Take the database offline + $resultDb = Suspend-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + + # Verify the change + $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' + $offlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } + + 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 -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + + 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } + + 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 | Suspend-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + } + } +} diff --git a/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..ab86a62b0a --- /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' { + { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + + $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 + + { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force } | Should -Not -Throw + + 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 + + { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + + $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' { + { Resume-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force } | Should -Not -Throw + + $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' { + { $mockServerObject | Resume-SqlDscDatabase -Name 'TestDatabase' -Force } | Should -Not -Throw + + $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' { + { $mockDatabaseObject | Resume-SqlDscDatabase -Force } | Should -Not -Throw + + $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' { + { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf } | Should -Not -Throw + + $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..06bc2c053a --- /dev/null +++ b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,298 @@ +<# + .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' { + { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + + $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 + + { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force } | Should -Not -Throw + + 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 + + { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + + $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' { + { Suspend-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force } | Should -Not -Throw + + $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' { + { $mockServerObject | Suspend-SqlDscDatabase -Name 'TestDatabase' -Force } | Should -Not -Throw + + $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' { + { $mockDatabaseObject | Suspend-SqlDscDatabase -Force } | Should -Not -Throw + + $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:setOfflineForceValue = $null + + # Override SetOffline to track the Force parameter + $mockDatabaseObject | Add-Member -MemberType ScriptMethod -Name SetOffline -Value { + param($ForceDisconnect = $false) + + $script:setOfflineForceValue = $ForceDisconnect + $this.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + } + + It 'Should call SetOffline with Force when Force parameter is specified' { + Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force -Confirm:$false + + $script:setOfflineForceValue | Should -BeTrue + } + + It 'Should call SetOffline without Force when Force parameter is not specified' { + $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal + + Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Confirm:$false + + $script:setOfflineForceValue | 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' { + { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf } | Should -Not -Throw + + $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' -Force } | Should -Throw -ExpectedMessage '*Failed to take database*offline*' + } + } +} diff --git a/tests/Unit/Stubs/SMO.cs b/tests/Unit/Stubs/SMO.cs index ea58a234a9..66cbedef0b 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -979,6 +979,16 @@ public void SetSnapshotIsolation( bool enable ) this.SnapshotIsolationState = SnapshotIsolationState.Disabled; } } + + public void SetOnline() + { + this.Status = DatabaseStatus.Normal; + } + + public void SetOffline(bool forceDisconnect = false) + { + this.Status = DatabaseStatus.Offline; + } } // TypeName: Microsoft.SqlServer.Management.Smo.FileGroup From 5920e4ca6be59ef279a24633d364af5155fa1f97 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 16:59:00 +0100 Subject: [PATCH 02/14] Add integration tests for Suspend and Resume SQL DSC Database commands --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) 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' From f98d2b4385cdec4de4f35121783ab2551677168e Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 17:01:25 +0100 Subject: [PATCH 03/14] Update CHANGELOG.md to enhance descriptions for `Resume-SqlDscDatabase` and `Suspend-SqlDscDatabase` commands --- CHANGELOG.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5f2807c19..4c1a3f87a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,15 +28,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added public command `Resume-SqlDscDatabase` to bring a SQL Server database back - online. This command uses the SMO `Database.SetOnline()` method to make databases - available after maintenance or downtime. The command supports both Server and - Database object pipeline input for flexible usage ([issue #2191](https://github.com/dsccommunity/SqlServerDsc/issues/2191)). -- Added public command `Suspend-SqlDscDatabase` to take a SQL Server database offline. - This command uses the SMO `Database.SetOffline()` method to temporarily make - databases unavailable for maintenance scenarios. The command includes a `Force` - parameter to disconnect active users when necessary and supports both Server and - Database object pipeline input ([issue #2192](https://github.com/dsccommunity/SqlServerDsc/issues/2192)). +- 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)). - Added public command `Enable-SqlDscDatabaseSnapshotIsolation` to enable snapshot isolation for a database in a SQL Server Database Engine instance. This command uses the SMO `SetSnapshotIsolation()` method to enable row-versioning and snapshot From 664fbcf01761a2601ded59c35890a0e4b5a2bb2f Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 17:03:25 +0100 Subject: [PATCH 04/14] Refactor tests for Resume-SqlDscDatabase and Suspend-SqlDscDatabase to suppress unnecessary throw checks --- tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 | 14 +++++++------- tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 index ab86a62b0a..2592afd585 100644 --- a/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 +++ b/tests/Unit/Public/Resume-SqlDscDatabase.Tests.ps1 @@ -119,7 +119,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { } It 'Should bring the database online and not throw' { - { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) @@ -129,7 +129,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { It 'Should call Get-SqlDscDatabase with Refresh when specified' { $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline - { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force } | Should -Not -Throw + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force Should -Invoke -CommandName 'Get-SqlDscDatabase' -ParameterFilter { $Refresh -eq $true @@ -148,7 +148,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { It 'Should not bring the database online when database is already online' { $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal - { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) } @@ -164,7 +164,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { } It 'Should bring the database online using DatabaseObject parameter' { - { Resume-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force } | Should -Not -Throw + $null = Resume-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) } @@ -193,7 +193,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { } It 'Should bring the database online via pipeline' { - { $mockServerObject | Resume-SqlDscDatabase -Name 'TestDatabase' -Force } | Should -Not -Throw + $null = $mockServerObject | Resume-SqlDscDatabase -Name 'TestDatabase' -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) } @@ -209,7 +209,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { } It 'Should bring the database online via pipeline' { - { $mockDatabaseObject | Resume-SqlDscDatabase -Force } | Should -Not -Throw + $null = $mockDatabaseObject | Resume-SqlDscDatabase -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) } @@ -229,7 +229,7 @@ Describe 'Resume-SqlDscDatabase' -Tag 'Public' { } It 'Should not bring the database online when WhatIf is specified' { - { Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf } | Should -Not -Throw + $null = Resume-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) } diff --git a/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 index 06bc2c053a..bd2dbaf1d8 100644 --- a/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 +++ b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 @@ -119,7 +119,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } It 'Should take the database offline and not throw' { - { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) @@ -129,7 +129,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { It 'Should call Get-SqlDscDatabase with Refresh when specified' { $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal - { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force } | Should -Not -Throw + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Refresh -Force Should -Invoke -CommandName 'Get-SqlDscDatabase' -ParameterFilter { $Refresh -eq $true @@ -148,7 +148,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { It 'Should not take the database offline when database is already offline' { $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline - { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Not -Throw + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) } @@ -164,7 +164,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } It 'Should take the database offline using DatabaseObject parameter' { - { Suspend-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force } | Should -Not -Throw + $null = Suspend-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) } @@ -193,7 +193,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } It 'Should take the database offline via pipeline' { - { $mockServerObject | Suspend-SqlDscDatabase -Name 'TestDatabase' -Force } | Should -Not -Throw + $null = $mockServerObject | Suspend-SqlDscDatabase -Name 'TestDatabase' -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) } @@ -209,7 +209,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } It 'Should take the database offline via pipeline' { - { $mockDatabaseObject | Suspend-SqlDscDatabase -Force } | Should -Not -Throw + $null = $mockDatabaseObject | Suspend-SqlDscDatabase -Force $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) } @@ -267,7 +267,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } It 'Should not take the database offline when WhatIf is specified' { - { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf } | Should -Not -Throw + $null = Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -WhatIf $mockDatabaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) } From 7d01b6ad4109d73791a0890f45c2f33c4a2edbba Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 17:05:29 +0100 Subject: [PATCH 05/14] Replace Write-Verbose with Write-Debug in Resume-SqlDscDatabase and Suspend-SqlDscDatabase functions for improved debugging output --- source/Public/Resume-SqlDscDatabase.ps1 | 6 +++--- source/Public/Suspend-SqlDscDatabase.ps1 | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/source/Public/Resume-SqlDscDatabase.ps1 b/source/Public/Resume-SqlDscDatabase.ps1 index fd2e0de882..a357cb01d6 100644 --- a/source/Public/Resume-SqlDscDatabase.ps1 +++ b/source/Public/Resume-SqlDscDatabase.ps1 @@ -150,11 +150,11 @@ function Resume-SqlDscDatabase # Check if database is already online (idempotence) if ($sqlDatabaseObject.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) { - Write-Verbose -Message ($script:localizedData.Database_AlreadyOnline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_AlreadyOnline -f $sqlDatabaseObject.Name) } else { - Write-Verbose -Message ($script:localizedData.Database_BringingOnline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_BringingOnline -f $sqlDatabaseObject.Name) try { @@ -174,7 +174,7 @@ function Resume-SqlDscDatabase ) } - Write-Verbose -Message ($script:localizedData.Database_BroughtOnline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_BroughtOnline -f $sqlDatabaseObject.Name) } if ($PassThru.IsPresent) diff --git a/source/Public/Suspend-SqlDscDatabase.ps1 b/source/Public/Suspend-SqlDscDatabase.ps1 index f28148f914..76e0acc294 100644 --- a/source/Public/Suspend-SqlDscDatabase.ps1 +++ b/source/Public/Suspend-SqlDscDatabase.ps1 @@ -153,17 +153,17 @@ function Suspend-SqlDscDatabase # Check if database is already offline (idempotence) if ($sqlDatabaseObject.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) { - Write-Verbose -Message ($script:localizedData.Database_AlreadyOffline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_AlreadyOffline -f $sqlDatabaseObject.Name) } else { if ($Force.IsPresent) { - Write-Verbose -Message ($script:localizedData.Database_TakingOfflineWithForce -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_TakingOfflineWithForce -f $sqlDatabaseObject.Name) } else { - Write-Verbose -Message ($script:localizedData.Database_TakingOffline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_TakingOffline -f $sqlDatabaseObject.Name) } try @@ -184,7 +184,7 @@ function Suspend-SqlDscDatabase ) } - Write-Verbose -Message ($script:localizedData.Database_TakenOffline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_TakenOffline -f $sqlDatabaseObject.Name) } if ($PassThru.IsPresent) From a64b2bf0510135477d64e2e9ee1cda795f324371 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 17:06:25 +0100 Subject: [PATCH 06/14] Remove specific error message expectation in Suspend-SqlDscDatabase integration test for non-existent database --- .../Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 index 033432e48c..8446a27975 100644 --- a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 @@ -107,7 +107,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL It 'Should throw error when trying to take offline non-existent database' { { Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction 'Stop' } | - Should -Throw -ExpectedMessage '*Database*NonExistentDatabase*not found*' + Should -Throw } } From 470799941853572b7961f36c70d8d95fc034b2c9 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 17:06:36 +0100 Subject: [PATCH 07/14] Remove specific error message expectation in Resume-SqlDscDatabase integration test for non-existent database --- .../Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 index 52c4588359..a4b98332fa 100644 --- a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -107,7 +107,7 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 It 'Should throw error when trying to bring online non-existent database' { { Resume-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction 'Stop' } | - Should -Throw -ExpectedMessage '*Database*NonExistentDatabase*not found*' + Should -Throw } } From 73f8b0c0241852587bc39dfd3620b3094929331d Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 20:07:45 +0100 Subject: [PATCH 08/14] Add process termination before taking database offline in Suspend-SqlDscDatabase function --- source/Public/Suspend-SqlDscDatabase.ps1 | 23 ++++++++++++++++++++++- source/en-US/SqlServerDsc.strings.psd1 | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/source/Public/Suspend-SqlDscDatabase.ps1 b/source/Public/Suspend-SqlDscDatabase.ps1 index 76e0acc294..5672f32e9f 100644 --- a/source/Public/Suspend-SqlDscDatabase.ps1 +++ b/source/Public/Suspend-SqlDscDatabase.ps1 @@ -160,6 +160,27 @@ function Suspend-SqlDscDatabase 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 { @@ -168,7 +189,7 @@ function Suspend-SqlDscDatabase try { - $sqlDatabaseObject.SetOffline($Force.IsPresent) + $sqlDatabaseObject.SetOffline() } catch { diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 9e5274b486..756d0eb862 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -384,8 +384,10 @@ ConvertFrom-StringData @' Database_AlreadyOffline = Database '{0}' is already offline. 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) From 145768685707c2ba89823c74119719786d1c721b Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 30 Nov 2025 20:13:24 +0100 Subject: [PATCH 09/14] Refactor Suspend-SqlDscDatabase tests to track KillAllProcesses calls and update mock behavior --- .../Public/Suspend-SqlDscDatabase.Tests.ps1 | 33 ++++++++++++------- tests/Unit/Stubs/SMO.cs | 6 +++- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 index bd2dbaf1d8..814e5abf62 100644 --- a/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 +++ b/tests/Unit/Public/Suspend-SqlDscDatabase.Tests.ps1 @@ -223,14 +223,13 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') $mockDatabaseObject.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal - $script:setOfflineForceValue = $null + $script:killAllProcessesCalled = $false - # Override SetOffline to track the Force parameter - $mockDatabaseObject | Add-Member -MemberType ScriptMethod -Name SetOffline -Value { - param($ForceDisconnect = $false) + # Override KillAllProcesses to track if it was called + $mockServerObject | Add-Member -MemberType ScriptMethod -Name KillAllProcesses -Value { + param($DatabaseName) - $script:setOfflineForceValue = $ForceDisconnect - $this.Status = [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline + $script:killAllProcessesCalled = $true } -Force Mock -CommandName 'Get-SqlDscDatabase' -MockWith { @@ -238,18 +237,21 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } } - It 'Should call SetOffline with Force when Force parameter is specified' { + It 'Should call KillAllProcesses when Force parameter is specified' { + $script:killAllProcessesCalled = $false + Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force -Confirm:$false - $script:setOfflineForceValue | Should -BeTrue + $script:killAllProcessesCalled | Should -BeTrue } - It 'Should call SetOffline without Force when Force parameter is not specified' { + 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:setOfflineForceValue | Should -BeFalse + $script:killAllProcessesCalled | Should -BeFalse } } @@ -292,7 +294,16 @@ Describe 'Suspend-SqlDscDatabase' -Tag 'Public' { } It 'Should throw a terminating error when SetOffline fails' { - { Suspend-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force } | Should -Throw -ExpectedMessage '*Failed to take database*offline*' + { 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 66cbedef0b..14450ae14f 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -517,6 +517,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; } @@ -985,7 +989,7 @@ public void SetOnline() this.Status = DatabaseStatus.Normal; } - public void SetOffline(bool forceDisconnect = false) + public void SetOffline() { this.Status = DatabaseStatus.Offline; } From 52060320d470ab2a5e8a021c5158439290062fb7 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 2 Dec 2025 16:35:35 +0100 Subject: [PATCH 10/14] Update database status checks in Resume-SqlDscDatabase and Suspend-SqlDscDatabase tests to use HasFlag for better accuracy --- ...Resume-SqlDscDatabase.Integration.Tests.ps1 | 6 +++--- ...uspend-SqlDscDatabase.Integration.Tests.ps1 | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 index a4b98332fa..b1f10785b9 100644 --- a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -63,7 +63,7 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 if ($existingDb) { # Ensure database is online before removing - if ($existingDb.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + if ($existingDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) { $null = Resume-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'SilentlyContinue' } @@ -82,7 +82,7 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Verify database is offline $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' - $offlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $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' @@ -118,7 +118,7 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Get the database object $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' - $databaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $databaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue # Bring the database online $resultDb = Resume-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' diff --git a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 index 8446a27975..0136690297 100644 --- a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 @@ -63,7 +63,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL if ($existingDb) { # Ensure database is online before removing - if ($existingDb.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + if ($existingDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) { $null = Resume-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'SilentlyContinue' } @@ -86,11 +86,11 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Take the database offline $resultDb = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } It 'Should be idempotent when database is already offline' { @@ -102,7 +102,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Verify the database is still offline $offlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' - $offlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } It 'Should throw error when trying to take offline non-existent database' { @@ -122,11 +122,11 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Take the database offline $resultDb = Suspend-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $offlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } } @@ -137,7 +137,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Take offline via pipeline $resultDb = $script:serverObject | Suspend-SqlDscDatabase -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } It 'Should take the database offline via pipeline using DatabaseObject' { @@ -147,7 +147,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } } @@ -158,7 +158,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Take offline using pipeline $resultDb = $script:serverObject | Get-SqlDscDatabase -Name $script:testDatabaseName | Suspend-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } } } From daeb33bd8a1b7a856bb9cddb3fafb89982f51e89 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 2 Dec 2025 16:36:53 +0100 Subject: [PATCH 11/14] Update CHANGELOG to consolidate and clarify the addition of Resume-SqlDscDatabase and Suspend-SqlDscDatabase commands --- CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6c0576514..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 @@ -62,13 +69,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- 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)). - Added public command `Enable-SqlDscDatabaseSnapshotIsolation` to enable snapshot isolation for a database in a SQL Server Database Engine instance. This command uses the SMO `SetSnapshotIsolation()` method to enable row-versioning and snapshot From 6fec844e25e581515cb3cd76d6964a3bc5c08bc8 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 2 Dec 2025 18:37:28 +0100 Subject: [PATCH 12/14] Enhance Resume-SqlDscDatabase and Suspend-SqlDscDatabase functions for better status handling and idempotence checks --- source/Public/Resume-SqlDscDatabase.ps1 | 16 ++++++++++--- source/Public/Suspend-SqlDscDatabase.ps1 | 14 +++++++++-- source/en-US/SqlServerDsc.strings.psd1 | 4 ++-- ...esume-SqlDscDatabase.Integration.Tests.ps1 | 24 ++++++++++++------- ...spend-SqlDscDatabase.Integration.Tests.ps1 | 8 ++++--- 5 files changed, 48 insertions(+), 18 deletions(-) diff --git a/source/Public/Resume-SqlDscDatabase.ps1 b/source/Public/Resume-SqlDscDatabase.ps1 index a357cb01d6..6323e8f0ed 100644 --- a/source/Public/Resume-SqlDscDatabase.ps1 +++ b/source/Public/Resume-SqlDscDatabase.ps1 @@ -147,10 +147,20 @@ function Resume-SqlDscDatabase if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) { - # Check if database is already online (idempotence) - if ($sqlDatabaseObject.Status -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + <# + 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) { - Write-Debug -Message ($script:localizedData.Database_AlreadyOnline -f $sqlDatabaseObject.Name) + $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 { diff --git a/source/Public/Suspend-SqlDscDatabase.ps1 b/source/Public/Suspend-SqlDscDatabase.ps1 index 5672f32e9f..66f7da173e 100644 --- a/source/Public/Suspend-SqlDscDatabase.ps1 +++ b/source/Public/Suspend-SqlDscDatabase.ps1 @@ -150,10 +150,20 @@ function Suspend-SqlDscDatabase 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 -eq [Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) + if ($sqlDatabaseObject.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline)) { - Write-Debug -Message ($script:localizedData.Database_AlreadyOffline -f $sqlDatabaseObject.Name) + Write-Debug -Message ($script:localizedData.Database_AlreadyOffline -f $sqlDatabaseObject.Name, ($sqlDatabaseObject.Status -join ', ')) } else { diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 756d0eb862..442d260e23 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -371,7 +371,7 @@ ConvertFrom-StringData @' 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. + 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. @@ -381,7 +381,7 @@ ConvertFrom-StringData @' 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. + 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}'. diff --git a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 index b1f10785b9..37a0873982 100644 --- a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -86,11 +86,13 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Bring the database online $resultDb = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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' { @@ -102,7 +104,8 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Verify the database is still online $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' - $onlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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' { @@ -122,11 +125,13 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Bring the database online $resultDb = Resume-SqlDscDatabase -DatabaseObject $databaseObject -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $onlineDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue } } @@ -137,7 +142,8 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Bring online via pipeline $resultDb = $script:serverObject | Resume-SqlDscDatabase -Name $script:testDatabaseName -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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' { @@ -147,7 +153,8 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # 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 | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeFalse + $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) | Should -BeTrue } } @@ -158,7 +165,8 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 # Bring online using pipeline $resultDb = $script:serverObject | Get-SqlDscDatabase -Name $script:testDatabaseName | Resume-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' - $resultDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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 index 0136690297..bb25316a87 100644 --- a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 @@ -82,7 +82,8 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Verify database is online $onlineDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Refresh -ErrorAction 'Stop' - $onlineDb.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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' @@ -118,7 +119,8 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL # Get the database object $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Refresh -ErrorAction 'Stop' - $databaseObject.Status | Should -Be ([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Normal) + $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' @@ -136,7 +138,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL $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 -PassThru -ErrorAction 'Stop' + $resultDb = $script:serverObject | Suspend-SqlDscDatabase -Name $script:testDatabaseName -Force -Refresh -PassThru -ErrorAction 'Stop' $resultDb.Status.HasFlag([Microsoft.SqlServer.Management.Smo.DatabaseStatus]::Offline) | Should -BeTrue } From b9735c6b0b4c733dbde4ca1ef1d66ffc55b2126a Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 2 Dec 2025 18:49:00 +0100 Subject: [PATCH 13/14] Update tests for Resume-SqlDscDatabase and Suspend-SqlDscDatabase to include Refresh parameter for improved accuracy --- .../Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 | 6 +++--- .../Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 index 37a0873982..716f92ee00 100644 --- a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -138,17 +138,17 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 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 -Force -ErrorAction 'Stop' + $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 -PassThru -ErrorAction 'Stop' + $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 -Force -ErrorAction 'Stop' + $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' diff --git a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 index bb25316a87..3907e7abd9 100644 --- a/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Suspend-SqlDscDatabase.Integration.Tests.ps1 @@ -159,7 +159,7 @@ Describe 'Suspend-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL $null = Resume-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' # Take offline using pipeline - $resultDb = $script:serverObject | Get-SqlDscDatabase -Name $script:testDatabaseName | Suspend-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $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 } } From 46a050a37914642de4c3950857669690010d7821 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Wed, 3 Dec 2025 10:23:22 +0100 Subject: [PATCH 14/14] Fix integ test --- .../Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 index 716f92ee00..fcf0cfa2b9 100644 --- a/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Resume-SqlDscDatabase.Integration.Tests.ps1 @@ -164,7 +164,7 @@ Describe 'Resume-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2 $null = Suspend-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction 'Stop' # Bring online using pipeline - $resultDb = $script:serverObject | Get-SqlDscDatabase -Name $script:testDatabaseName | Resume-SqlDscDatabase -Force -PassThru -ErrorAction 'Stop' + $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 }