diff --git a/CHANGELOG.md b/CHANGELOG.md index f12e38fa5c..193621e8ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `Get-SqlDscAgentAlert`, `New-SqlDscAgentAlert`, `Set-SqlDscAgentAlert`, `Remove-SqlDscAgentAlert`, and `Test-SqlDscAgentAlert` to manage SQL Agent alerts on a Database Engine instance. +- Added new public commands for database management: + - `Get-SqlDscDatabase` - Get databases from a SQL Server Database Engine instance + - `New-SqlDscDatabase` - Create a new database with specified properties + - `Set-SqlDscDatabase` - Modify properties of an existing database + - `Remove-SqlDscDatabase` - Remove a database from SQL Server instance + - `Test-SqlDscDatabase` - Test if a database is in the desired state + - All commands support pipeline input with ServerObject and follow established patterns + - Database objects can also be used as pipeline input for Set and Remove operations + - Commands include comprehensive validation, localization, and ShouldProcess support ### Changed diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b007ecf0ec..ffc76a1ded 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -301,6 +301,11 @@ stages: 'tests/Integration/Commands/Get-SqlDscRole.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRole.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscLogin.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscDatabase.Integration.Tests.ps1' + 'tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1' + 'tests/Integration/Commands/Set-SqlDscDatabase.Integration.Tests.ps1' + 'tests/Integration/Commands/Test-SqlDscDatabase.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscDatabase.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscAgentAlert.Integration.Tests.ps1' 'tests/Integration/Commands/New-SqlDscAgentAlert.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscAgentAlert.Integration.Tests.ps1' diff --git a/source/Public/Get-SqlDscDatabase.ps1 b/source/Public/Get-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..c6d5016e52 --- /dev/null +++ b/source/Public/Get-SqlDscDatabase.ps1 @@ -0,0 +1,103 @@ +<# + .SYNOPSIS + Get databases from a SQL Server Database Engine instance. + + .DESCRIPTION + This command gets one or more databases from a SQL Server Database Engine instance. + If no name is specified, all databases are returned. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the database to get. If not specified, all + databases are returned. + + .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. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Get-SqlDscDatabase + + Get all databases from the instance. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' + + Get the database named **MyDatabase**. + + .OUTPUTS + `[Microsoft.SqlServer.Management.Smo.Database[]]` +#> +function Get-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/8.')] + [OutputType([Microsoft.SqlServer.Management.Smo.Database[]])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + process + { + if ($Refresh.IsPresent) + { + # Refresh the server object's databases collection + $ServerObject.Databases.Refresh() + } + + Write-Verbose -Message ($script:localizedData.Database_Get -f $ServerObject.InstanceName) + + $databaseObject = @() + + if ($PSBoundParameters.ContainsKey('Name')) + { + $databaseObject = $ServerObject.Databases[$Name] + + if (-not $databaseObject) + { + Write-Verbose -Message ($script:localizedData.Database_NotFound -f $Name) + + $missingDatabaseMessage = $script:localizedData.Database_NotFound -f $Name + + $writeErrorParameters = @{ + Message = $missingDatabaseMessage + Category = 'ObjectNotFound' + ErrorId = 'GSDD0001' # cspell: disable-line + TargetObject = $Name + } + + Write-Error @writeErrorParameters + } + else + { + Write-Verbose -Message ($script:localizedData.Database_Found -f $Name) + } + } + else + { + Write-Verbose -Message ($script:localizedData.Database_GetAll) + + $databaseObject = $ServerObject.Databases + } + + return [Microsoft.SqlServer.Management.Smo.Database[]] $databaseObject + } +} diff --git a/source/Public/New-SqlDscDatabase.ps1 b/source/Public/New-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..3eee2586dc --- /dev/null +++ b/source/Public/New-SqlDscDatabase.ps1 @@ -0,0 +1,206 @@ +<# + .SYNOPSIS + Creates a new database in a SQL Server Database Engine instance. + + .DESCRIPTION + This command creates a new database in a SQL Server Database Engine instance. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the database to be created. + + .PARAMETER Collation + The name of the SQL collation to use for the new database. + Default value is server collation. + + .PARAMETER CompatibilityLevel + The version of the SQL compatibility level to use for the new database. + Default value is server version. + + .PARAMETER RecoveryModel + The recovery model to be used for the new database. + Default value is Full. + + .PARAMETER OwnerName + Specifies the name of the login that should be the owner of the database. + + .PARAMETER Force + Specifies that the database should be created without any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + creating 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. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | New-SqlDscDatabase -Name 'MyDatabase' + + Creates a new database named **MyDatabase**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | New-SqlDscDatabase -Name 'MyDatabase' -Collation 'SQL_Latin1_General_Pref_CP850_CI_AS' -RecoveryModel 'Simple' -Force + + Creates a new database named **MyDatabase** with the specified collation and recovery model + without prompting for confirmation. + + .OUTPUTS + `[Microsoft.SqlServer.Management.Smo.Database]` +#> +function New-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/8.')] + [OutputType([Microsoft.SqlServer.Management.Smo.Database])] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Collation, + + [Parameter()] + [ValidateSet('Version80', 'Version90', 'Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150', 'Version160')] + [System.String] + $CompatibilityLevel, + + [Parameter()] + [ValidateSet('Simple', 'Full', 'BulkLogged')] + [System.String] + $RecoveryModel, + + [Parameter()] + [System.String] + $OwnerName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + begin + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + } + + process + { + if ($Refresh.IsPresent) + { + # Refresh the server object's databases collection + $ServerObject.Databases.Refresh() + } + + Write-Verbose -Message ($script:localizedData.Database_Create -f $Name, $ServerObject.InstanceName) + + # Check if the database already exists + if ($ServerObject.Databases[$Name]) + { + $errorMessage = $script:localizedData.Database_AlreadyExists -f $Name, $ServerObject.InstanceName + New-InvalidOperationException -Message $errorMessage + } + + # Validate compatibility level if specified + if ($PSBoundParameters.ContainsKey('CompatibilityLevel')) + { + $supportedCompatibilityLevels = @{ + 8 = @('Version80') + 9 = @('Version80', 'Version90') + 10 = @('Version80', 'Version90', 'Version100') + 11 = @('Version90', 'Version100', 'Version110') + 12 = @('Version100', 'Version110', 'Version120') + 13 = @('Version100', 'Version110', 'Version120', 'Version130') + 14 = @('Version100', 'Version110', 'Version120', 'Version130', 'Version140') + 15 = @('Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150') + 16 = @('Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150', 'Version160') + } + + if ($CompatibilityLevel -notin $supportedCompatibilityLevels.$($ServerObject.VersionMajor)) + { + $errorMessage = $script:localizedData.Database_InvalidCompatibilityLevel -f $CompatibilityLevel, $ServerObject.InstanceName + New-InvalidArgumentException -ArgumentName 'CompatibilityLevel' -Message $errorMessage + } + } + + # Validate collation if specified + if ($PSBoundParameters.ContainsKey('Collation')) + { + if ($Collation -notin $ServerObject.EnumCollations().Name) + { + $errorMessage = $script:localizedData.Database_InvalidCollation -f $Collation, $ServerObject.InstanceName + New-InvalidArgumentException -ArgumentName 'Collation' -Message $errorMessage + } + } + + $verboseDescriptionMessage = $script:localizedData.Database_Create_ShouldProcessVerboseDescription -f $Name, $ServerObject.InstanceName + $verboseWarningMessage = $script:localizedData.Database_Create_ShouldProcessVerboseWarning -f $Name + $captionMessage = $script:localizedData.Database_Create_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + try + { + $sqlDatabaseObjectToCreate = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList $ServerObject, $Name + + if ($PSBoundParameters.ContainsKey('RecoveryModel')) + { + $sqlDatabaseObjectToCreate.RecoveryModel = $RecoveryModel + } + + if ($PSBoundParameters.ContainsKey('Collation')) + { + $sqlDatabaseObjectToCreate.Collation = $Collation + } + + if ($PSBoundParameters.ContainsKey('CompatibilityLevel')) + { + $sqlDatabaseObjectToCreate.CompatibilityLevel = $CompatibilityLevel + } + + Write-Verbose -Message ($script:localizedData.Database_Creating -f $Name) + + $sqlDatabaseObjectToCreate.Create() + + <# + This must be run after the object is created because + the owner property is read-only and the method cannot + be call until the object has been created. + #> + if ($PSBoundParameters.ContainsKey('OwnerName')) + { + $sqlDatabaseObjectToCreate.SetOwner($OwnerName) + } + + Write-Verbose -Message ($script:localizedData.Database_Created -f $Name) + + return $sqlDatabaseObjectToCreate + } + catch + { + $errorMessage = $script:localizedData.Database_CreateFailed -f $Name, $ServerObject.InstanceName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } +} diff --git a/source/Public/Remove-SqlDscDatabase.ps1 b/source/Public/Remove-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..40475227a7 --- /dev/null +++ b/source/Public/Remove-SqlDscDatabase.ps1 @@ -0,0 +1,142 @@ +<# + .SYNOPSIS + Removes a database from a SQL Server Database Engine instance. + + .DESCRIPTION + This command removes a database from a SQL Server Database Engine instance. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER DatabaseObject + Specifies a database object to remove. + + .PARAMETER Name + Specifies the name of the database to be removed. + + .PARAMETER Force + Specifies that the database should be removed without any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + trying to remove 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, or pass in **DatabaseObject**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $databaseObject = $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' + $databaseObject | Remove-SqlDscDatabase + + Removes the database named **MyDatabase**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Remove-SqlDscDatabase -Name 'MyDatabase' + + Removes the database named **MyDatabase**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Remove-SqlDscDatabase -Name 'MyDatabase' -Force + + Removes the database named **MyDatabase** without prompting for confirmation. + + .OUTPUTS + None. +#> +function Remove-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/8.')] + [OutputType()] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param + ( + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'DatabaseObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Database] + $DatabaseObject, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObject')] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + process + { + if ($PSCmdlet.ParameterSetName -eq 'ServerObject') + { + if ($Refresh.IsPresent) + { + # Refresh the server object's databases collection + $ServerObject.Databases.Refresh() + } + + Write-Verbose -Message ($script:localizedData.Database_Remove -f $Name, $ServerObject.InstanceName) + + # Check if the database is a system database (cannot be dropped) + $systemDatabases = @('master', 'model', 'msdb', 'tempdb') + if ($Name -in $systemDatabases) + { + $errorMessage = $script:localizedData.Database_CannotRemoveSystem -f $Name + New-InvalidOperationException -Message $errorMessage + } + + # Get the database object + $DatabaseObject = $ServerObject.Databases[$Name] + + if (-not $DatabaseObject) + { + $errorMessage = $script:localizedData.Database_NotFound -f $Name + New-InvalidOperationException -Message $errorMessage + } + } + else + { + $Name = $DatabaseObject.Name + Write-Verbose -Message ($script:localizedData.Database_Remove -f $Name, $DatabaseObject.Parent.InstanceName) + + # Check if the database is a system database (cannot be dropped) + $systemDatabases = @('master', 'model', 'msdb', 'tempdb') + if ($Name -in $systemDatabases) + { + $errorMessage = $script:localizedData.Database_CannotRemoveSystem -f $Name + New-InvalidOperationException -Message $errorMessage + } + } + + $verboseDescriptionMessage = $script:localizedData.Database_Remove_ShouldProcessVerboseDescription -f $Name, $DatabaseObject.Parent.InstanceName + $verboseWarningMessage = $script:localizedData.Database_Remove_ShouldProcessVerboseWarning -f $Name + $captionMessage = $script:localizedData.Database_Remove_ShouldProcessCaption + + if ($Force.IsPresent -or $PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + try + { + Write-Verbose -Message ($script:localizedData.Database_Removing -f $Name) + + $DatabaseObject.Drop() + + Write-Verbose -Message ($script:localizedData.Database_Removed -f $Name) + } + catch + { + $errorMessage = $script:localizedData.Database_RemoveFailed -f $Name, $DatabaseObject.Parent.InstanceName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } +} diff --git a/source/Public/Set-SqlDscDatabase.ps1 b/source/Public/Set-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..afd5893f46 --- /dev/null +++ b/source/Public/Set-SqlDscDatabase.ps1 @@ -0,0 +1,228 @@ +<# + .SYNOPSIS + Sets properties of a database in a SQL Server Database Engine instance. + + .DESCRIPTION + This command sets properties of a database in a SQL Server Database Engine instance. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER DatabaseObject + Specifies a database object to modify. + + .PARAMETER Name + Specifies the name of the database to be modified. + + .PARAMETER Collation + The name of the SQL collation to set for the database. + + .PARAMETER CompatibilityLevel + The version of the SQL compatibility level to set for the database. + + .PARAMETER RecoveryModel + The recovery model to be set for the database. + + .PARAMETER OwnerName + Specifies the name of the login that should be the owner of the database. + + .PARAMETER Force + Specifies that the database should be modified without any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + modifying 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, or pass in **DatabaseObject**. + + .PARAMETER PassThru + Specifies that the database object should be returned after modification. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $databaseObject = $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' + $databaseObject | Set-SqlDscDatabase -RecoveryModel 'Simple' + + Sets the recovery model of the database named **MyDatabase** to **Simple**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Set-SqlDscDatabase -Name 'MyDatabase' -OwnerName 'sa' -Force + + Sets the owner of the database named **MyDatabase** to **sa** without prompting for confirmation. + + .OUTPUTS + None. But when **PassThru** is specified the output is `[Microsoft.SqlServer.Management.Smo.Database]`. +#> +function Set-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/8.')] + [OutputType()] + [OutputType([Microsoft.SqlServer.Management.Smo.Database])] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param + ( + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(ParameterSetName = 'DatabaseObject', Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Database] + $DatabaseObject, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Collation, + + [Parameter()] + [ValidateSet('Version80', 'Version90', 'Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150', 'Version160')] + [System.String] + $CompatibilityLevel, + + [Parameter()] + [ValidateSet('Simple', 'Full', 'BulkLogged')] + [System.String] + $RecoveryModel, + + [Parameter()] + [System.String] + $OwnerName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObject')] + [System.Management.Automation.SwitchParameter] + $Refresh, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru + ) + + process + { + if ($PSCmdlet.ParameterSetName -eq 'ServerObject') + { + if ($Refresh.IsPresent) + { + # Refresh the server object's databases collection + $ServerObject.Databases.Refresh() + } + + Write-Verbose -Message ($script:localizedData.Database_Set -f $Name, $ServerObject.InstanceName) + + # Get the database object + $DatabaseObject = $ServerObject.Databases[$Name] + + if (-not $DatabaseObject) + { + $errorMessage = $script:localizedData.Database_NotFound -f $Name + New-InvalidOperationException -Message $errorMessage + } + } + else + { + $Name = $DatabaseObject.Name + $ServerObject = $DatabaseObject.Parent + Write-Verbose -Message ($script:localizedData.Database_Set -f $Name, $ServerObject.InstanceName) + } + + # Validate compatibility level if specified + if ($PSBoundParameters.ContainsKey('CompatibilityLevel')) + { + $supportedCompatibilityLevels = @{ + 8 = @('Version80') + 9 = @('Version80', 'Version90') + 10 = @('Version80', 'Version90', 'Version100') + 11 = @('Version90', 'Version100', 'Version110') + 12 = @('Version100', 'Version110', 'Version120') + 13 = @('Version100', 'Version110', 'Version120', 'Version130') + 14 = @('Version100', 'Version110', 'Version120', 'Version130', 'Version140') + 15 = @('Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150') + 16 = @('Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150', 'Version160') + } + + if ($CompatibilityLevel -notin $supportedCompatibilityLevels.$($ServerObject.VersionMajor)) + { + $errorMessage = $script:localizedData.Database_InvalidCompatibilityLevel -f $CompatibilityLevel, $ServerObject.InstanceName + New-ObjectNotFoundException -Message $errorMessage + } + } + + # Validate collation if specified + if ($PSBoundParameters.ContainsKey('Collation')) + { + if ($Collation -notin $ServerObject.EnumCollations().Name) + { + $errorMessage = $script:localizedData.Database_InvalidCollation -f $Collation, $ServerObject.InstanceName + New-ObjectNotFoundException -Message $errorMessage + } + } + + $verboseDescriptionMessage = $script:localizedData.Database_Set_ShouldProcessVerboseDescription -f $Name, $ServerObject.InstanceName + $verboseWarningMessage = $script:localizedData.Database_Set_ShouldProcessVerboseWarning -f $Name + $captionMessage = $script:localizedData.Database_Set_ShouldProcessCaption + + if ($Force.IsPresent -or $PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + try + { + $wasUpdate = $false + + if ($PSBoundParameters.ContainsKey('Collation')) + { + Write-Verbose -Message ($script:localizedData.Database_UpdatingCollation -f $Collation) + $DatabaseObject.Collation = $Collation + $wasUpdate = $true + } + + if ($PSBoundParameters.ContainsKey('CompatibilityLevel')) + { + Write-Verbose -Message ($script:localizedData.Database_UpdatingCompatibilityLevel -f $CompatibilityLevel) + $DatabaseObject.CompatibilityLevel = $CompatibilityLevel + $wasUpdate = $true + } + + if ($PSBoundParameters.ContainsKey('RecoveryModel')) + { + Write-Verbose -Message ($script:localizedData.Database_UpdatingRecoveryModel -f $RecoveryModel) + $DatabaseObject.RecoveryModel = $RecoveryModel + $wasUpdate = $true + } + + if ($PSBoundParameters.ContainsKey('OwnerName')) + { + Write-Verbose -Message ($script:localizedData.Database_UpdatingOwner -f $OwnerName) + $DatabaseObject.SetOwner($OwnerName) + $wasUpdate = $true + } + + if ($wasUpdate) + { + Write-Verbose -Message ($script:localizedData.Database_Updating -f $Name) + $DatabaseObject.Alter() + Write-Verbose -Message ($script:localizedData.Database_Updated -f $Name) + } + + if ($PassThru.IsPresent) + { + return $DatabaseObject + } + } + catch + { + $errorMessage = $script:localizedData.Database_SetFailed -f $Name, $ServerObject.InstanceName + New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ + } + } + } +} diff --git a/source/Public/Test-SqlDscDatabase.ps1 b/source/Public/Test-SqlDscDatabase.ps1 new file mode 100644 index 0000000000..9eb05fb87b --- /dev/null +++ b/source/Public/Test-SqlDscDatabase.ps1 @@ -0,0 +1,168 @@ +<# + .SYNOPSIS + Tests if a database on a SQL Server Database Engine instance is in the desired state. + + .DESCRIPTION + This command tests if a database on a SQL Server Database Engine instance is in the desired state. + + .PARAMETER ServerObject + Specifies current server connection object. + + .PARAMETER Name + Specifies the name of the database to test. + + .PARAMETER Ensure + When set to 'Present', the database must exist. + When set to 'Absent', the database must not exist. + + .PARAMETER Collation + The name of the SQL collation that the database should have. + + .PARAMETER CompatibilityLevel + The version of the SQL compatibility level that the database should have. + + .PARAMETER RecoveryModel + The recovery model that the database should have. + + .PARAMETER OwnerName + Specifies the name of the login that should be the owner of the database. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + testing the database state. 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. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Test-SqlDscDatabase -Name 'MyDatabase' -Ensure 'Present' + + Tests if the database named **MyDatabase** exists. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | Test-SqlDscDatabase -Name 'MyDatabase' -Ensure 'Present' -RecoveryModel 'Simple' -OwnerName 'sa' + + Tests if the database named **MyDatabase** exists and has the specified recovery model and owner. + + .OUTPUTS + `[System.Boolean]` +#> +function Test-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/8.')] + [OutputType([System.Boolean])] + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.Server] + $ServerObject, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [ValidateNotNullOrEmpty()] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [System.String] + $Collation, + + [Parameter()] + [ValidateSet('Version80', 'Version90', 'Version100', 'Version110', 'Version120', 'Version130', 'Version140', 'Version150', 'Version160')] + [System.String] + $CompatibilityLevel, + + [Parameter()] + [ValidateSet('Simple', 'Full', 'BulkLogged')] + [System.String] + $RecoveryModel, + + [Parameter()] + [System.String] + $OwnerName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + process + { + if ($Refresh.IsPresent) + { + # Refresh the server object's databases collection + $ServerObject.Databases.Refresh() + } + + Write-Verbose -Message ($script:localizedData.Database_Test -f $Name, $ServerObject.InstanceName) + + $isDatabaseInDesiredState = $true + + # Check database exists + $sqlDatabaseObject = $ServerObject.Databases[$Name] + + switch ($Ensure) + { + 'Absent' + { + if ($sqlDatabaseObject) + { + Write-Verbose -Message ($script:localizedData.Database_NotInDesiredStateAbsent -f $Name) + $isDatabaseInDesiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.Database_InDesiredStateAbsent -f $Name) + } + } + + 'Present' + { + if (-not $sqlDatabaseObject) + { + Write-Verbose -Message ($script:localizedData.Database_NotInDesiredStatePresent -f $Name) + $isDatabaseInDesiredState = $false + } + else + { + Write-Verbose -Message ($script:localizedData.Database_InDesiredStatePresent -f $Name) + + if ($PSBoundParameters.ContainsKey('Collation') -and $sqlDatabaseObject.Collation -ne $Collation) + { + Write-Verbose -Message ($script:localizedData.Database_CollationWrong -f $Name, $sqlDatabaseObject.Collation, $Collation) + $isDatabaseInDesiredState = $false + } + + if ($PSBoundParameters.ContainsKey('CompatibilityLevel') -and $sqlDatabaseObject.CompatibilityLevel -ne $CompatibilityLevel) + { + Write-Verbose -Message ($script:localizedData.Database_CompatibilityLevelWrong -f $Name, $sqlDatabaseObject.CompatibilityLevel, $CompatibilityLevel) + $isDatabaseInDesiredState = $false + } + + if ($PSBoundParameters.ContainsKey('RecoveryModel') -and $sqlDatabaseObject.RecoveryModel -ne $RecoveryModel) + { + Write-Verbose -Message ($script:localizedData.Database_RecoveryModelWrong -f $Name, $sqlDatabaseObject.RecoveryModel, $RecoveryModel) + $isDatabaseInDesiredState = $false + } + + if ($PSBoundParameters.ContainsKey('OwnerName') -and $sqlDatabaseObject.Owner -ne $OwnerName) + { + Write-Verbose -Message ($script:localizedData.Database_OwnerNameWrong -f $Name, $sqlDatabaseObject.Owner, $OwnerName) + $isDatabaseInDesiredState = $false + } + } + } + } + + return $isDatabaseInDesiredState + } +} diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index c4e6f719a1..05392261d5 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -289,6 +289,61 @@ ConvertFrom-StringData @' Login_Add_LoginCreated = Successfully created login '{0}' on the instance '{1}'. Login_Add_LoginAlreadyExists = The login '{0}' already exists on the instance '{1}'. + ## Get-SqlDscDatabase + Database_Get = Getting databases from instance '{0}'. + Database_GetAll = Getting all databases. + Database_Found = Found database '{0}'. + Database_NotFound = Database '{0}' was not found. + + ## New-SqlDscDatabase + Database_Create = Creating database '{0}' on instance '{1}'. + Database_Creating = Creating database '{0}'. + Database_Created = Database '{0}' was created successfully. + Database_CreateFailed = Failed to create database '{0}' on instance '{1}'. + Database_AlreadyExists = Database '{0}' already exists on instance '{1}'. + Database_InvalidCompatibilityLevel = The specified compatibility level '{0}' is not a valid compatibility level for the instance '{1}'. + Database_InvalidCollation = The specified collation '{0}' is not a valid collation for the instance '{1}'. + Database_Create_ShouldProcessVerboseDescription = Creating the database '{0}' on the instance '{1}'. + Database_Create_ShouldProcessVerboseWarning = Are you sure you want to create the database '{0}'? + # 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 + + ## Set-SqlDscDatabase + Database_Set = Setting properties of database '{0}' on instance '{1}'. + Database_Updating = Updating database '{0}'. + Database_Updated = Database '{0}' was updated successfully. + Database_SetFailed = Failed to set properties of database '{0}' on instance '{1}'. + Database_UpdatingCollation = Changing the database collation to '{0}'. + Database_UpdatingCompatibilityLevel = Changing the database compatibility level to '{0}'. + Database_UpdatingRecoveryModel = Changing the database recovery model to '{0}'. + Database_UpdatingOwner = Changing the database owner to '{0}'. + Database_Set_ShouldProcessVerboseDescription = Setting properties of the database '{0}' on the instance '{1}'. + Database_Set_ShouldProcessVerboseWarning = Are you sure you want to modify the database '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Database_Set_ShouldProcessCaption = Set database properties on instance + + ## Remove-SqlDscDatabase + Database_Remove = Removing database '{0}' from instance '{1}'. + Database_Removing = Removing database '{0}'. + Database_Removed = Database '{0}' was removed successfully. + Database_RemoveFailed = Failed to remove database '{0}' from instance '{1}'. + Database_CannotRemoveSystem = Cannot remove system database '{0}'. + Database_Remove_ShouldProcessVerboseDescription = Removing the database '{0}' from the instance '{1}'. + Database_Remove_ShouldProcessVerboseWarning = Are you sure you want to remove the database '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Database_Remove_ShouldProcessCaption = Remove database from instance + + ## Test-SqlDscDatabase + Database_Test = Testing the state of database '{0}' on instance '{1}'. + Database_InDesiredStatePresent = Database '{0}' is present and in desired state. + Database_InDesiredStateAbsent = Database '{0}' is absent as expected. + Database_NotInDesiredStatePresent = Expected the database '{0}' to be present, but it was absent. + Database_NotInDesiredStateAbsent = Expected the database '{0}' to be absent, but it was present. + Database_CollationWrong = The database '{0}' exists and has the collation '{1}', but expected it to have the collation '{2}'. + Database_CompatibilityLevelWrong = The database '{0}' exists and has the compatibility level '{1}', but expected it to have the compatibility level '{2}'. + Database_RecoveryModelWrong = The database '{0}' exists and has the recovery model '{1}', but expected it to have the recovery model '{2}'. + Database_OwnerNameWrong = The database '{0}' exists and has the owner '{1}', but expected it to have the owner '{2}'. + ## Get-AgentAlertObject Get_AgentAlertObject_GettingAlert = Getting SQL Agent Alert '{0}'. (GAAO0001) diff --git a/tests/Integration/Commands/Get-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..d262663dd3 --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,106 @@ +[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 build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName +} + +Describe 'Get-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + # Starting the named instance SQL Server service prior to running tests. + Start-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception. + $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 + } + + AfterAll { + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject + + # Stop the named instance SQL Server service to save memory on the build worker. + Stop-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + } + + Context 'When getting all SQL Server databases' { + It 'Should return an array of Database objects' { + $result = Get-SqlDscDatabase -ServerObject $script:serverObject + + <# + Casting to array to ensure we get the count on Windows PowerShell + when there is only one database. + #> + @($result).Count | Should -BeGreaterOrEqual 1 + @($result)[0] | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + } + + It 'Should return system databases including master' { + $result = Get-SqlDscDatabase -ServerObject $script:serverObject + + $result.Name | Should -Contain 'master' + $result.Name | Should -Contain 'model' + $result.Name | Should -Contain 'msdb' + $result.Name | Should -Contain 'tempdb' + } + } + + Context 'When getting a specific SQL Server database' { + It 'Should return the specified database when it exists (system database)' { + $result = Get-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'master' + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + } + + It 'Should throw error when getting a non-existent database' { + { Get-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -ErrorAction 'Stop' } | + Should -Throw + } + + It 'Should return nothing when getting a non-existent database with SilentlyContinue' { + $result = Get-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -ErrorAction 'SilentlyContinue' + + $result | Should -BeNullOrEmpty + } + } + + Context 'When using the Refresh parameter' { + It 'Should refresh the database collection and return databases' { + $result = Get-SqlDscDatabase -ServerObject $script:serverObject -Refresh + + @($result).Count | Should -BeGreaterOrEqual 1 + $result.Name | Should -Contain 'master' + } + } +} diff --git a/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..4a57ecec12 --- /dev/null +++ b/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,126 @@ +[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 build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' +} + +Describe 'New-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + # Starting the named instance SQL Server service prior to running tests. + Start-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception. + $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 = 'SqlDscTestDatabase_' + (Get-Random) + $script:testDatabaseNameWithProperties = 'SqlDscTestDatabaseWithProps_' + (Get-Random) + } + + AfterAll { + # Clean up test databases + $testDatabasesToRemove = @($script:testDatabaseName, $script:testDatabaseNameWithProperties) + + foreach ($dbName in $testDatabasesToRemove) + { + $existingDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $dbName -ErrorAction 'SilentlyContinue' + + if ($existingDb) + { + $null = Remove-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'Stop' + } + } + + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject + + # Stop the named instance SQL Server service to save memory on the build worker. + Stop-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + } + + Context 'When creating a new database' { + It 'Should create a database successfully with minimal parameters' { + $result = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction Stop + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:testDatabaseName + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + + # Verify the database exists + $createdDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction Stop + $createdDb | Should -Not -BeNullOrEmpty + } + + It 'Should create a database with specified properties' { + $result = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameWithProperties -RecoveryModel 'Simple' -Force -ErrorAction Stop + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:testDatabaseNameWithProperties + $result.RecoveryModel | Should -Be 'Simple' + + # Verify the database exists with correct properties + $createdDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameWithProperties -ErrorAction Stop + $createdDb | Should -Not -BeNullOrEmpty + $createdDb.RecoveryModel | Should -Be 'Simple' + } + + It 'Should throw error when trying to create a database that already exists' { + { New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction Stop } | + Should -Throw + } + } + + Context 'When using the Refresh parameter' { + It 'Should refresh the database collection before creating' { + $uniqueName = 'SqlDscTestRefresh_' + (Get-Random) + + try + { + $result = New-SqlDscDatabase -ServerObject $script:serverObject -Name $uniqueName -Refresh -Force -ErrorAction Stop + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $uniqueName + } + finally + { + # Clean up + $dbToRemove = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $uniqueName -ErrorAction 'SilentlyContinue' + if ($dbToRemove) + { + $null = Remove-SqlDscDatabase -DatabaseObject $dbToRemove -Force + } + } + } + } +} diff --git a/tests/Integration/Commands/Remove-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..b81dff88ed --- /dev/null +++ b/tests/Integration/Commands/Remove-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,151 @@ +[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 build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName +} + +Describe 'Remove-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + # Starting the named instance SQL Server service prior to running tests. + Start-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception. + $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 + } + + AfterAll { + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject + + # Stop the named instance SQL Server service to save memory on the build worker. + Stop-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + } + + Context 'When removing a database using ServerObject parameter set' { + BeforeEach { + # Create a test database for each test + $script:testDatabaseName = 'SqlDscTestRemoveDatabase_' + (Get-Random) + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction Stop + } + + It 'Should remove a database successfully' { + # Verify database exists before removal + $existingDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction Stop + $existingDb | Should -Not -BeNullOrEmpty + + # Remove the database + $null = Remove-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Force -ErrorAction Stop + + # Verify database no longer exists + $removedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction 'SilentlyContinue' + $removedDb | Should -BeNullOrEmpty + } + + It 'Should throw error when trying to remove non-existent database' { + { Remove-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Force -ErrorAction Stop } | + Should -Throw + } + } + + Context 'When removing a database using DatabaseObject parameter set' { + BeforeEach { + # Create a test database for each test + $script:testDatabaseNameForObject = 'SqlDscTestRemoveDatabaseObj_' + (Get-Random) + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -Force -ErrorAction Stop + } + + It 'Should remove a database using database object' { + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction Stop + $databaseObject | Should -Not -BeNullOrEmpty + + # Remove the database using database object + $null = Remove-SqlDscDatabase -DatabaseObject $databaseObject -Force -ErrorAction Stop + + # Verify database no longer exists + $removedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'SilentlyContinue' + $removedDb | Should -BeNullOrEmpty + } + + It 'Should support pipeline input with database object' { + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction Stop + $databaseObject | Should -Not -BeNullOrEmpty + + # Remove the database using pipeline + $databaseObject | Remove-SqlDscDatabase -Force -ErrorAction Stop + + # Verify database no longer exists + $removedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'SilentlyContinue' + $removedDb | Should -BeNullOrEmpty + } + } + + Context 'When attempting to remove system databases' { + It 'Should throw error when trying to remove master database' { + { Remove-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' -Force -ErrorAction Stop } | + Should -Throw + } + + It 'Should throw error when trying to remove model database' { + { Remove-SqlDscDatabase -ServerObject $script:serverObject -Name 'model' -Force -ErrorAction Stop } | + Should -Throw + } + + It 'Should throw error when trying to remove msdb database' { + { Remove-SqlDscDatabase -ServerObject $script:serverObject -Name 'msdb' -Force -ErrorAction Stop } | + Should -Throw + } + + It 'Should throw error when trying to remove tempdb database' { + { Remove-SqlDscDatabase -ServerObject $script:serverObject -Name 'tempdb' -Force -ErrorAction Stop } | + Should -Throw + } + } + + Context 'When using the Refresh parameter' { + BeforeEach { + # Create a test database for each test + $script:testDatabaseNameRefresh = 'SqlDscTestRemoveDatabaseRefresh_' + (Get-Random) + $null = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameRefresh -Force -ErrorAction Stop + } + + It 'Should refresh the database collection before removing' { + # Remove the database with refresh + $null = Remove-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameRefresh -Refresh -Force -ErrorAction Stop + + # Verify database no longer exists + $removedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameRefresh -ErrorAction 'SilentlyContinue' + $removedDb | Should -BeNullOrEmpty + } + } +} diff --git a/tests/Integration/Commands/Set-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Set-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..e475d9e9f6 --- /dev/null +++ b/tests/Integration/Commands/Set-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,159 @@ +[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 build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName +} + +Describe 'Set-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + # Starting the named instance SQL Server service prior to running tests. + Start-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception. + $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 + + # Test database names + $script:testDatabaseName = 'SqlDscTestSetDatabase_' + (Get-Random) + $script:testDatabaseNameForObject = 'SqlDscTestSetDatabaseObj_' + (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' + } + + 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) + { + $null = Remove-SqlDscDatabase -DatabaseObject $existingDb -Force -ErrorAction 'Stop' + } + } + + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject + + # Stop the named instance SQL Server service to save memory on the build worker. + Stop-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + } + + Context 'When setting database properties using ServerObject parameter set' { + It 'Should set recovery model successfully' { + $null = Set-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -RecoveryModel 'Simple' -Force -ErrorAction 'Stop' + + # Verify the change + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName + $updatedDb.RecoveryModel | Should -Be 'Simple' + } + + It 'Should set owner name successfully' { + $null = Set-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -OwnerName ('{0}\SqlAdmin' -f $script:mockComputerName) -Force -ErrorAction 'Stop' + + # Verify the change + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction 'Stop' + $updatedDb.Owner | Should -Be ('{0}\SqlAdmin' -f $script:mockComputerName) + } + + It 'Should set multiple properties successfully' { + $null = Set-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -RecoveryModel 'Full' -OwnerName ('{0}\SqlAdmin' -f $script:mockComputerName) -Force -ErrorAction 'Stop' + + # Verify the changes + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction 'Stop' + $updatedDb.RecoveryModel | Should -Be 'Full' + $updatedDb.Owner | Should -Be ('{0}\SqlAdmin' -f $script:mockComputerName) + } + + It 'Should throw error when trying to set properties of non-existent database' { + { Set-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -RecoveryModel 'Simple' -Force -ErrorAction 'Stop' } | + Should -Throw + } + } + + Context 'When setting database properties using DatabaseObject parameter set' { + It 'Should set recovery model using database object' { + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'Stop' + + $null = Set-SqlDscDatabase -DatabaseObject $databaseObject -RecoveryModel 'Simple' -Force -ErrorAction 'Stop' + + # Verify the change + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'Stop' + $updatedDb.RecoveryModel | Should -Be 'Simple' + } + + It 'Should set owner name using database object' { + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'Stop' + + $null = Set-SqlDscDatabase -DatabaseObject $databaseObject -OwnerName ('{0}\SqlAdmin' -f $script:mockComputerName) -Force -ErrorAction 'Stop' + + # Verify the change + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'Stop' + $updatedDb.Owner | Should -Be ('{0}\SqlAdmin' -f $script:mockComputerName) + } + + It 'Should support pipeline input with database object' { + $databaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'Stop' + $null = $databaseObject | Set-SqlDscDatabase -RecoveryModel 'Full' -Force -ErrorAction 'Stop' + + # Verify the change + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseNameForObject -ErrorAction 'Stop' + $updatedDb.RecoveryModel | Should -Be 'Full' + } + } + + Context 'When using the Refresh parameter' { + It 'Should refresh the database collection before setting properties' { + $null = Set-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -RecoveryModel 'BulkLogged' -Refresh -Force -ErrorAction 'Stop' + + # Verify the change + $updatedDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -ErrorAction 'Stop' + $updatedDb.RecoveryModel | Should -Be 'BulkLogged' + } + } + + Context 'When using the PassThru parameter' { + It 'Should return the database object when PassThru is specified' { + $result = Set-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -RecoveryModel 'Simple' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.Name | Should -Be $script:testDatabaseName + $result.RecoveryModel | Should -Be 'Simple' + } + } +} diff --git a/tests/Integration/Commands/Test-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/Test-SqlDscDatabase.Integration.Tests.ps1 new file mode 100644 index 0000000000..a592d4c992 --- /dev/null +++ b/tests/Integration/Commands/Test-SqlDscDatabase.Integration.Tests.ps1 @@ -0,0 +1,104 @@ +[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 build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' +} + +Describe 'Test-SqlDscDatabase' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + # Starting the named instance SQL Server service prior to running tests. + Start-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + + $script:mockInstanceName = 'DSCSQLTEST' + $script:mockComputerName = Get-ComputerName + + $mockSqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception. + $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' + } + + AfterAll { + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject -ErrorAction 'Stop' + + # Stop the named instance SQL Server service to save memory on the build worker. + Stop-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop' + } + + Context 'When testing database presence' { + It 'Should return true when system database exists and Ensure is Present' { + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' -Ensure 'Present' -ErrorAction 'Stop' + + $result | Should -BeTrue + } + + It 'Should return false when non-existent database is tested with Ensure Present' { + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Ensure 'Present' -ErrorAction 'Stop' + + $result | Should -BeFalse + } + + It 'Should return false when system database exists and Ensure is Absent' { + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' -Ensure 'Absent' -ErrorAction 'Stop' + + $result | Should -BeFalse + } + + It 'Should return true when non-existent database is tested with Ensure Absent' { + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'NonExistentDatabase' -Ensure 'Absent' -ErrorAction 'Stop' + + $result | Should -BeTrue + } + } + + Context 'When testing database properties' { + It 'Should return true when testing master database with correct recovery model' { + # Master database typically has Simple recovery model + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' -Ensure 'Present' -RecoveryModel 'Simple' -ErrorAction 'Stop' + + $result | Should -BeTrue + } + + It 'Should return false when testing master database with incorrect recovery model' { + # Master database typically has Simple recovery model, so Full should return false + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' -Ensure 'Present' -RecoveryModel 'Full' -ErrorAction 'Stop' + + $result | Should -BeFalse + } + } + + Context 'When using the Refresh parameter' { + It 'Should refresh the database collection and test database presence' { + $result = Test-SqlDscDatabase -ServerObject $script:serverObject -Name 'master' -Ensure 'Present' -Refresh -ErrorAction 'Stop' + + $result | Should -BeTrue + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Get-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..37f212db18 --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,165 @@ +[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 build" 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 'Get-SqlDscDatabase' -Tag 'Public' { + Context 'When getting all databases' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + $databaseCollection = @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'master')), + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase')) + ) + return $databaseCollection | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + } + + It 'Should return all databases when no Name parameter is specified' { + $result = Get-SqlDscDatabase -ServerObject $mockServerObject + + $result | Should -HaveCount 2 + $result[0].Name | Should -Be 'master' + $result[1].Name | Should -Be 'TestDatabase' + } + + It 'Should call Refresh when Refresh parameter is specified' { + $script:refreshCalled = $false + $mockServerObjectWithRefresh = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectWithRefresh | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectWithRefresh | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + $databaseCollection = @( + (New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObjectWithRefresh, 'master')) + ) + return $databaseCollection | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + $script:refreshCalled = $true + } -PassThru -Force + } -Force + + $result = Get-SqlDscDatabase -ServerObject $mockServerObjectWithRefresh -Refresh + + $result | Should -HaveCount 1 + $script:refreshCalled | Should -BeTrue + } + } + + Context 'When getting a specific database' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'master' = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'master') + 'TestDatabase' = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'TestDatabase') + } | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + } + + It 'Should return the specified database when it exists' { + $result = Get-SqlDscDatabase -ServerObject $mockServerObject -Name 'master' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'master' + } + + It 'Should throw the correct error when the specified database does not exist' { + # Test the exact error message first + $expectedMessage = 'Database ''NonExistentDatabase'' was not found.' + + { Get-SqlDscDatabase -ServerObject $mockServerObject -Name 'NonExistentDatabase' -ErrorAction 'Stop' } | + Should -Throw -ExpectedMessage $expectedMessage + } + + It 'Should return empty result when ignoring the error' { + $result = Get-SqlDscDatabase -ServerObject $mockServerObject -Name 'NonExistentDatabase' -ErrorAction 'SilentlyContinue' + + $result | Should -BeNullOrEmpty + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set __AllParameterSets' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-ServerObject] [[-Name] ] [-Refresh] []' + } + ) { + $result = (Get-Command -Name 'Get-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 + } + + It 'Should have ServerObject as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscDatabase').Parameters['ServerObject'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Name as a non-mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscDatabase').Parameters['Name'] + $parameterInfo.Attributes.Mandatory | Should -BeFalse + } + + It 'Should have Refresh as a non-mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscDatabase').Parameters['Refresh'] + $parameterInfo.Attributes.Mandatory | Should -BeFalse + } + } +} \ No newline at end of file diff --git a/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..3d0ab41705 --- /dev/null +++ b/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,174 @@ +[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 build" 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 'New-SqlDscDatabase' -Tag 'Public' { + Context 'When creating a new database' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{} | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + $mockServerObject | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + + Mock -CommandName 'New-Object' -ParameterFilter { $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Database' } -MockWith { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $ArgumentList[1] -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'RecoveryModel' -Value $null -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Collation' -Value $null -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'CompatibilityLevel' -Value $null -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + # Mock implementation + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'SetOwner' -Value { + param($OwnerName) + # Mock implementation + } -Force + return $mockDatabaseObject + } + } + + It 'Should create a database successfully with minimal parameters' { + $result = New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestDatabase' + Should -Invoke -CommandName 'New-Object' -ParameterFilter { $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Database' } -Exactly -Times 1 + } + + It 'Should create a database with specified properties' { + $result = New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase2' -Collation 'SQL_Latin1_General_CP1_CI_AS' -RecoveryModel 'Simple' -CompatibilityLevel 'Version150' -OwnerName 'sa' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestDatabase2' + $result.RecoveryModel | Should -Be 'Simple' + $result.Collation | Should -Be 'SQL_Latin1_General_CP1_CI_AS' + $result.CompatibilityLevel | Should -Be 'Version150' + } + + It 'Should throw error when database already exists' { + $mockServerObjectWithExistingDb = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectWithExistingDb | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectWithExistingDb | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'ExistingDatabase' = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + } + } -Force + + { New-SqlDscDatabase -ServerObject $mockServerObjectWithExistingDb -Name 'ExistingDatabase' -Force } | + Should -Throw -ExpectedMessage '*already exists*' + } + } + + Context 'When testing parameter validation errors' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{} + } -Force + $mockServerObject | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + } + + It 'Should throw error when CompatibilityLevel is invalid for SQL Server version' { + { New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDB' -CompatibilityLevel 'Version80' -Force } | + Should -Throw -ExpectedMessage '*not a valid compatibility level*' + } + + It 'Should throw error when Collation is invalid' { + { New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDB' -Collation 'InvalidCollation' -Force } | + Should -Throw -ExpectedMessage '*not a valid collation*' + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set __AllParameterSets' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-ServerObject] [-Name] [[-Collation] ] [[-CompatibilityLevel] ] [[-RecoveryModel] ] [[-OwnerName] ] [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-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 + } + + It 'Should have ServerObject as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDatabase').Parameters['ServerObject'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDatabase').Parameters['Name'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + } +} \ No newline at end of file diff --git a/tests/Unit/Public/Remove-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Remove-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..7cac54d2fb --- /dev/null +++ b/tests/Unit/Public/Remove-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,160 @@ +[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 build" 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 'Remove-SqlDscDatabase' -Tag 'Public' { + Context 'When removing a database using ServerObject and Name' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'Parent' -Value { + $mockParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + return $mockParent + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value { + # Mock implementation + } -Force + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'TestDatabase' = $mockDatabaseObject + 'master' = $mockDatabaseObject + } | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + } + + It 'Should remove database successfully' { + $null = Remove-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Force + } + + It 'Should throw error when database does not exist' { + { Remove-SqlDscDatabase -ServerObject $mockServerObject -Name 'NonExistentDatabase' -Force } | + Should -Throw -ExpectedMessage '*not found*' + } + + It 'Should throw error when trying to remove system database' { + { Remove-SqlDscDatabase -ServerObject $mockServerObject -Name 'master' -Force } | + Should -Throw -ExpectedMessage '*Cannot remove system database*' + } + } + + Context 'When removing a database using DatabaseObject' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'Parent' -Value { + $mockParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + return $mockParent + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value { + # Mock implementation + } -Force + } + + It 'Should remove database successfully using database object' { + $null = Remove-SqlDscDatabase -DatabaseObject $mockDatabaseObject -Force + } + + It 'Should throw error when trying to remove system database using DatabaseObject' { + $mockSystemDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSystemDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'master' -Force + $mockSystemDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'Parent' -Value { + $mockParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + return $mockParent + } -Force + + { Remove-SqlDscDatabase -DatabaseObject $mockSystemDatabaseObject -Force } | + Should -Throw -ExpectedMessage '*Cannot remove system database*' + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set ServerObject' -ForEach @( + @{ + ExpectedParameterSetName = 'ServerObject' + ExpectedParameters = '-ServerObject -Name [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Remove-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 + } + + It 'Should have the correct parameters in parameter set DatabaseObject' -ForEach @( + @{ + ExpectedParameterSetName = 'DatabaseObject' + ExpectedParameters = '-DatabaseObject [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Remove-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 + } + } +} \ No newline at end of file diff --git a/tests/Unit/Public/Set-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Set-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..b975bd4005 --- /dev/null +++ b/tests/Unit/Public/Set-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,317 @@ +[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 build" 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 'Set-SqlDscDatabase' -Tag 'Public' { + Context 'When modifying a database using ServerObject and Name' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Collation' -Value 'SQL_Latin1_General_CP1_CI_AS' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'RecoveryModel' -Value 'Full' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'CompatibilityLevel' -Value 'Version150' -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'Parent' -Value { + $mockParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockParent | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + return $mockParent + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + # Mock implementation + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'SetOwner' -Value { + param($OwnerName) + # Mock implementation + } -Force + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'TestDatabase' = $mockDatabaseObject + } | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + $mockServerObject | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + } + + It 'Should modify database properties successfully' { + $null = Set-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -RecoveryModel 'Simple' -Force + } + + It 'Should return database object when PassThru is specified' { + $result = Set-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -RecoveryModel 'Simple' -Force -PassThru + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestDatabase' + } + + It 'Should throw error when database does not exist' { + { Set-SqlDscDatabase -ServerObject $mockServerObject -Name 'NonExistentDatabase' -RecoveryModel 'Simple' -Force } | + Should -Throw -ExpectedMessage '*not found*' + } + } + + Context 'When modifying a database using DatabaseObject' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Collation' -Value 'SQL_Latin1_General_CP1_CI_AS' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'RecoveryModel' -Value 'Full' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'CompatibilityLevel' -Value 'Version150' -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'Parent' -Value { + $mockParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockParent | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + return $mockParent + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + # Mock implementation + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'SetOwner' -Value { + param($OwnerName) + # Mock implementation + } -Force + } + + It 'Should modify database using database object' { + $null = Set-SqlDscDatabase -DatabaseObject $mockDatabaseObject -RecoveryModel 'Simple' -Force + } + } + + Context 'When testing CompatibilityLevel parameter validation' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + return @{ + 'TestDatabase' = $mockDatabaseObject + } + } -Force + $mockServerObject | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' } + ) + } -Force + } + + It 'Should throw error when CompatibilityLevel is invalid for SQL Server version' { + { Set-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -CompatibilityLevel 'Version80' -Force } | + Should -Throw -ExpectedMessage '*not a valid compatibility level*' + } + + It 'Should allow valid CompatibilityLevel for SQL Server version' { + # We only test that the validation passes, not the actual property setting + $mockServerObjectWithValidDb = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectWithValidDb | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectWithValidDb | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockDatabaseObjectWithValidProps = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObjectWithValidProps | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObjectWithValidProps | Add-Member -MemberType 'NoteProperty' -Name 'CompatibilityLevel' -Value 'Version150' -Force + $mockDatabaseObjectWithValidProps | Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + # Mock implementation + } -Force + $mockServerObjectWithValidDb | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'TestDatabase' = $mockDatabaseObjectWithValidProps + } + } -Force + $mockServerObjectWithValidDb | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' } + ) + } -Force + + $null = Set-SqlDscDatabase -ServerObject $mockServerObjectWithValidDb -Name 'TestDatabase' -CompatibilityLevel 'Version150' -Force + } + } + + Context 'When testing Collation parameter validation' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + # Mock implementation + } -Force + return @{ + 'TestDatabase' = $mockDatabaseObject + } + } -Force + $mockServerObject | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + } + + It 'Should throw error when Collation is invalid' { + { Set-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Collation 'InvalidCollation' -Force } | + Should -Throw -ExpectedMessage '*not a valid collation*' + } + + It 'Should allow valid Collation' { + $mockServerObjectWithValidDb = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectWithValidDb | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectWithValidDb | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockDatabaseObjectWithValidProps = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObjectWithValidProps | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObjectWithValidProps | Add-Member -MemberType 'NoteProperty' -Name 'Collation' -Value 'SQL_Latin1_General_CP1_CI_AS' -Force + $mockDatabaseObjectWithValidProps | Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + # Mock implementation + } -Force + $mockServerObjectWithValidDb | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'TestDatabase' = $mockDatabaseObjectWithValidProps + } + } -Force + $mockServerObjectWithValidDb | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' }, + @{ Name = 'SQL_Latin1_General_Pref_CP850_CI_AS' } + ) + } -Force + + $null = Set-SqlDscDatabase -ServerObject $mockServerObjectWithValidDb -Name 'TestDatabase' -Collation 'SQL_Latin1_General_CP1_CI_AS' -Force + } + } + + Context 'When testing OwnerName parameter usage' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'Parent' -Value { + $mockParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockParent | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + $mockParent | Add-Member -MemberType 'ScriptMethod' -Name 'EnumCollations' -Value { + return @( + @{ Name = 'SQL_Latin1_General_CP1_CI_AS' } + ) + } -Force + return $mockParent + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { + # Mock implementation + } -Force + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'SetOwner' -Value { + param($OwnerName) + # Mock implementation + } -Force + } + + It 'Should call SetOwner when OwnerName parameter is specified' { + # This tests that the OwnerName parameter usage path (line 202) is covered + $null = Set-SqlDscDatabase -DatabaseObject $mockDatabaseObject -OwnerName 'sa' -Force + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set ServerObject' -ForEach @( + @{ + ExpectedParameterSetName = 'ServerObject' + ExpectedParameters = '-ServerObject -Name [-Collation ] [-CompatibilityLevel ] [-RecoveryModel ] [-OwnerName ] [-Force] [-Refresh] [-PassThru] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Set-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 + } + + It 'Should have the correct parameters in parameter set DatabaseObject' -ForEach @( + @{ + ExpectedParameterSetName = 'DatabaseObject' + ExpectedParameters = '-DatabaseObject [-Collation ] [-CompatibilityLevel ] [-RecoveryModel ] [-OwnerName ] [-Force] [-PassThru] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Set-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 + } + } +} \ No newline at end of file diff --git a/tests/Unit/Public/Test-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/Test-SqlDscDatabase.Tests.ps1 new file mode 100644 index 0000000000..3bcc02e87a --- /dev/null +++ b/tests/Unit/Public/Test-SqlDscDatabase.Tests.ps1 @@ -0,0 +1,186 @@ +[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 build" 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 'Test-SqlDscDatabase' -Tag 'Public' { + Context 'When testing database presence' { + BeforeAll { + $mockExistingDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Collation' -Value 'SQL_Latin1_General_CP1_CI_AS' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'CompatibilityLevel' -Value 'Version150' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'RecoveryModel' -Value 'Full' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Owner' -Value 'sa' -Force + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'TestDatabase' = $mockExistingDatabase + } | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + } + + It 'Should return true when database exists and Ensure is Present' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Present' + + $result | Should -BeTrue + } + + It 'Should return false when database does not exist and Ensure is Present' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'NonExistentDatabase' -Ensure 'Present' + + $result | Should -BeFalse + } + + It 'Should return false when database exists and Ensure is Absent' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Absent' + + $result | Should -BeFalse + } + + It 'Should return true when database does not exist and Ensure is Absent' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'NonExistentDatabase' -Ensure 'Absent' + + $result | Should -BeTrue + } + } + + Context 'When testing database properties' { + BeforeAll { + $mockExistingDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Collation' -Value 'SQL_Latin1_General_CP1_CI_AS' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'CompatibilityLevel' -Value 'Version150' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'RecoveryModel' -Value 'Full' -Force + $mockExistingDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Owner' -Value 'sa' -Force + + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'TestDatabase' = $mockExistingDatabase + } | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + } + + It 'Should return true when all properties match' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Present' -Collation 'SQL_Latin1_General_CP1_CI_AS' -CompatibilityLevel 'Version150' -RecoveryModel 'Full' -OwnerName 'sa' + + $result | Should -BeTrue + } + + It 'Should return false when collation does not match' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Present' -Collation 'Different_Collation' + + $result | Should -BeFalse + } + + It 'Should return false when compatibility level does not match' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Present' -CompatibilityLevel 'Version140' + + $result | Should -BeFalse + } + + It 'Should return false when recovery model does not match' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Present' -RecoveryModel 'Simple' + + $result | Should -BeFalse + } + + It 'Should return false when owner does not match' { + + $result = Test-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDatabase' -Ensure 'Present' -OwnerName 'DifferentOwner' + + $result | Should -BeFalse + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set __AllParameterSets' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-ServerObject] [-Name] [[-Ensure] ] [[-Collation] ] [[-CompatibilityLevel] ] [[-RecoveryModel] ] [[-OwnerName] ] [-Refresh] []' + } + ) { + $result = (Get-Command -Name 'Test-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 + } + + It 'Should have ServerObject as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Test-SqlDscDatabase').Parameters['ServerObject'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Test-SqlDscDatabase').Parameters['Name'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + } +} \ No newline at end of file