diff --git a/.github/instructions/dsc-community-style-guidelines-pester.instructions.md b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md index 08ef42c190..55193b9f1f 100644 --- a/.github/instructions/dsc-community-style-guidelines-pester.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md @@ -28,7 +28,7 @@ applyTo: "**/*.[Tt]ests.ps1" - Mock variables prefix: 'mock' ## Structure & Scope -- Public commands: Never use `InModuleScope` (unless retrieving localized strings) +- Public commands: Never use `InModuleScope` (unless retrieving localized strings or creating an object using an internal class) - Private functions/class resources: Always use `InModuleScope` - Each class method = separate `Context` block - Each scenario = separate `Context` block @@ -46,6 +46,7 @@ applyTo: "**/*.[Tt]ests.ps1" - Set `$PSDefaultParameterValues` for `Mock:ModuleName`, `Should:ModuleName`, `InModuleScope:ModuleName` - Omit `-ModuleName` parameter on Pester commands - Never use `Mock` inside `InModuleScope`-block +- Never use `param()` inside `-MockWith` scriptblocks, parameters are auto-bound ## File Organization - Class resources: `tests/Unit/Classes/{Name}.Tests.ps1` diff --git a/.github/instructions/dsc-community-style-guidelines.instructions.md b/.github/instructions/dsc-community-style-guidelines.instructions.md index f0d5748090..5bab164664 100644 --- a/.github/instructions/dsc-community-style-guidelines.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines.instructions.md @@ -19,6 +19,8 @@ applyTo: "**" ## File Organization - Public commands: `source/Public/{CommandName}.ps1` - Private functions: `source/Private/{FunctionName}.ps1` +- Classes: `source/Classes/{DependencyGroupNumber}.{ClassName}.ps1` +- Enums: `source/Enum/{DependencyGroupNumber}.{EnumName}.ps1` - Unit tests: `tests/Unit/{Classes|Public|Private}/{Name}.Tests.ps1` - Integration tests: `tests/Integration/Commands/{CommandName}.Integration.Tests.ps1` diff --git a/.vscode/settings.json b/.vscode/settings.json index 14bbda0996..68a53391b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -173,5 +173,26 @@ "path": "bash", "args": [] } + }, + "chat.tools.terminal.terminalProfile.osx": { + "path": "pwsh", + "args": [], + "env": { + "COPILOT": "1" + } + }, + "chat.tools.terminal.terminalProfile.linux": { + "path": "pwsh", + "args": [], + "env": { + "COPILOT": "1" + } + }, + "chat.tools.terminal.terminalProfile.windows": { + "path": "pwsh.exe", + "args": [], + "env": { + "COPILOT": "1" + } } } diff --git a/CHANGELOG.md b/CHANGELOG.md index b60ec3a5f8..40d184ab80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added public command `New-SqlDscDatabaseSnapshot` to create database snapshots + in a SQL Server Database Engine instance using SMO. This command provides an + automated and DSC-friendly approach to snapshot management by leveraging + `New-SqlDscDatabase` for the actual creation. The command now supports `FileGroup` + and `DataFile` parameters to allow control over snapshot file placement and + structure ([issue #2341](https://github.com/dsccommunity/SqlServerDsc/issues/2341)). +- Added public command `New-SqlDscFileGroup` to create FileGroup objects for SQL + Server databases. This command simplifies creating FileGroup objects that can be + used with `New-SqlDscDatabase` and other database-related commands. The `Database` + parameter is optional, allowing FileGroup objects to be created standalone and + added to a Database later using `Add-SqlDscFileGroup`. +- Added public command `New-SqlDscDataFile` to create DataFile objects for SQL + Server FileGroups. This command simplifies creating DataFile objects with + specified physical file paths, supporting both regular database files (.mdf, .ndf) + and sparse files for database snapshots (.ss). The `FileGroup` parameter is + mandatory, requiring DataFile objects to be created with an associated FileGroup. +- Added public command `Add-SqlDscFileGroup` to add one or more FileGroup objects + to a Database. This command provides a clean way to associate FileGroup objects + with a Database after they have been created. +- Added public command `ConvertTo-SqlDscDataFile` to convert `DatabaseFileSpec` + objects to SMO DataFile objects. +- Added public command `ConvertTo-SqlDscFileGroup` to convert `DatabaseFileGroupSpec` + objects to SMO FileGroup objects. +- Added class `DatabaseFileSpec` to define data file specifications without requiring + a database or SMO context. +- Added class `DatabaseFileGroupSpec` to define file group specifications with + associated data files without requiring a database or SMO context. +- `New-SqlDscDatabase` + - Added `FileGroup` and `DataFile` parameters to allow specifying custom file + locations and structure. These parameters apply to both regular databases and + database snapshots, enabling control over file placement for snapshots (sparse + files) and custom filegroup/datafile configuration for regular databases + ([issue #2341](https://github.com/dsccommunity/SqlServerDsc/issues/2341)). - Added public command `Set-SqlDscDatabaseOwner` to change the owner of a SQL Server database [issue #2177](https://github.com/dsccommunity/SqlServerDsc/issues/2177). This command uses the SMO `SetOwner()` method and supports both `ServerObject` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f88596af6b..c73555db99 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -293,6 +293,11 @@ stages: # Group 2 'tests/Integration/Commands/PostInstallationConfiguration.Integration.Tests.ps1' # Group 3 + 'tests/Integration/Commands/New-SqlDscFileGroup.Integration.Tests.ps1' + 'tests/Integration/Commands/New-SqlDscDataFile.Integration.Tests.ps1' + 'tests/Integration/Commands/Add-SqlDscFileGroup.Integration.Tests.ps1' + 'tests/Integration/Commands/ConvertTo-SqlDscFileGroup.Integration.Tests.ps1' + 'tests/Integration/Commands/ConvertTo-SqlDscDataFile.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscSetupLog.Integration.Tests.ps1' 'tests/Integration/Commands/Connect-SqlDscDatabaseEngine.Integration.Tests.ps1' 'tests/Integration/Commands/Disconnect-SqlDscDatabaseEngine.Integration.Tests.ps1' @@ -335,6 +340,7 @@ stages: 'tests/Integration/Commands/Get-SqlDscDatabase.Integration.Tests.ps1' 'tests/Integration/Commands/ConvertFrom-SqlDscDatabasePermission.Integration.Tests.ps1' 'tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1' + 'tests/Integration/Commands/New-SqlDscDatabaseSnapshot.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscCompatibilityLevel.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscDatabaseProperty.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscDatabaseOwner.Integration.Tests.ps1' diff --git a/source/Classes/002.DatabaseFileSpec.ps1 b/source/Classes/002.DatabaseFileSpec.ps1 new file mode 100644 index 0000000000..6f024a65a2 --- /dev/null +++ b/source/Classes/002.DatabaseFileSpec.ps1 @@ -0,0 +1,97 @@ +<# + .SYNOPSIS + Defines a data file specification for a database file group. + + .DESCRIPTION + This class represents a data file specification that can be used when + creating a new database. It contains the properties needed to define + a data file without requiring an existing database or file group SMO object. + + .PARAMETER Name + The logical name of the data file. + + .PARAMETER FileName + The physical file path for the data file. This must be a valid path + on the SQL Server instance. + + .PARAMETER Size + The initial size of the data file in kilobytes. If not specified, + SQL Server will use its default initial size. + + .PARAMETER MaxSize + The maximum size to which the data file can grow in kilobytes. + If not specified, the file can grow without limit (or up to disk space). + + .PARAMETER Growth + The amount by which the data file grows when it needs more space. + The value is in kilobytes if GrowthType is KB, or a percentage if + GrowthType is Percent. If not specified, SQL Server will use its + default growth setting. + + .PARAMETER GrowthType + Specifies whether the Growth value is in kilobytes (KB) or percent (Percent). + If not specified, defaults to KB. + + .PARAMETER IsPrimaryFile + Specifies that this file is the primary file in the PRIMARY file group. + Only one file in the PRIMARY file group should be marked as the primary file. + This property is typically used for the first file in the PRIMARY file group. + + .NOTES + This class is used to specify data file configurations when creating a new + database via New-SqlDscDatabase. Unlike SMO DataFile objects, these + specification objects can be created without an existing database context. + + .EXAMPLE + $fileSpec = [DatabaseFileSpec]::new() + $fileSpec.Name = 'MyDatabase_Data' + $fileSpec.FileName = 'C:\SQLData\MyDatabase.mdf' + $fileSpec.Size = 102400 # 100 MB in KB + $fileSpec.Growth = 10240 # 10 MB in KB + $fileSpec.GrowthType = 'KB' + + Creates a new data file specification with a specific size and growth settings. + + .EXAMPLE + [DatabaseFileSpec] @{ + Name = 'MyDatabase_Data' + FileName = 'C:\SQLData\MyDatabase.mdf' + IsPrimaryFile = $true + } + + Creates a new primary data file specification using hashtable syntax. +#> +class DatabaseFileSpec +{ + [System.String] + $Name + + [System.String] + $FileName + + [System.Nullable[System.Double]] + $Size + + [System.Nullable[System.Double]] + $MaxSize + + [System.Nullable[System.Double]] + $Growth + + [ValidateSet('KB', 'MB', 'Percent')] + [System.String] + $GrowthType + + [System.Boolean] + $IsPrimaryFile = $false + + DatabaseFileSpec() + { + } + + DatabaseFileSpec([System.String] $name, [System.String] $fileName) + { + $this.Name = $name + $this.FileName = $fileName + } +} diff --git a/source/Classes/004.DatabaseFileGroupSpec.ps1 b/source/Classes/004.DatabaseFileGroupSpec.ps1 new file mode 100644 index 0000000000..83389c674a --- /dev/null +++ b/source/Classes/004.DatabaseFileGroupSpec.ps1 @@ -0,0 +1,110 @@ +<# + .SYNOPSIS + Defines a file group specification for a database. + + .DESCRIPTION + This class represents a file group specification that can be used when + creating a new database. It contains the properties needed to define + a file group and its associated data files without requiring an existing + database SMO object. + + .PARAMETER Name + The name of the file group. For the primary file group, this should be 'PRIMARY'. + + .PARAMETER Files + An array of DatabaseFileSpec objects that define the data files belonging + to this file group. At least one file must be specified for each file group. + + .PARAMETER ReadOnly + Specifies whether the file group is read-only. If not specified, defaults + to $false (read-write). + + .PARAMETER IsDefault + Specifies whether this file group should be the default file group for + new objects. If not specified, defaults to $false. Typically, only the + PRIMARY file group or one custom file group should be marked as default. + + .NOTES + This class is used to specify file group configurations when creating a new + database via New-SqlDscDatabase. Unlike SMO FileGroup objects, these + specification objects can be created without an existing database context. + + When creating a database, you typically need at least one file group named + 'PRIMARY' which contains the primary data file. Additional file groups can + be added for organizing data files. + + .EXAMPLE + $primaryFile = [DatabaseFileSpec] @{ + Name = 'MyDatabase_Primary' + FileName = 'C:\SQLData\MyDatabase.mdf' + IsPrimaryFile = $true + } + + $primaryFileGroup = [DatabaseFileGroupSpec]::new() + $primaryFileGroup.Name = 'PRIMARY' + $primaryFileGroup.Files = @($primaryFile) + + Creates a PRIMARY file group specification with one primary data file. + + .EXAMPLE + $dataFile1 = [DatabaseFileSpec] @{ + Name = 'MyDatabase_Data1' + FileName = 'D:\SQLData\MyDatabase_Data1.ndf' + Size = 204800 # 200 MB + } + + $dataFile2 = [DatabaseFileSpec] @{ + Name = 'MyDatabase_Data2' + FileName = 'D:\SQLData\MyDatabase_Data2.ndf' + Size = 204800 # 200 MB + } + + $secondaryFileGroup = [DatabaseFileGroupSpec] @{ + Name = 'SECONDARY' + Files = @($dataFile1, $dataFile2) + } + + Creates a SECONDARY file group specification with two data files. + + .EXAMPLE + [DatabaseFileGroupSpec] @{ + Name = 'PRIMARY' + Files = @( + [DatabaseFileSpec] @{ + Name = 'MyDB_Primary' + FileName = 'C:\SQLData\MyDB.mdf' + } + ) + } + + Creates a PRIMARY file group using hashtable syntax with an embedded file spec. +#> +class DatabaseFileGroupSpec +{ + [System.String] + $Name + + [DatabaseFileSpec[]] + $Files + + [System.Boolean] + $ReadOnly = $false + + [System.Boolean] + $IsDefault = $false + + DatabaseFileGroupSpec() + { + } + + DatabaseFileGroupSpec([System.String] $name) + { + $this.Name = $name + } + + DatabaseFileGroupSpec([System.String] $name, [DatabaseFileSpec[]] $files) + { + $this.Name = $name + $this.Files = $files + } +} diff --git a/source/Public/Add-SqlDscFileGroup.ps1 b/source/Public/Add-SqlDscFileGroup.ps1 new file mode 100644 index 0000000000..e81f0e0c69 --- /dev/null +++ b/source/Public/Add-SqlDscFileGroup.ps1 @@ -0,0 +1,96 @@ +<# + .SYNOPSIS + Adds one or more FileGroup objects to a Database object. + + .DESCRIPTION + This command adds one or more FileGroup objects to a Database object's FileGroups + collection. This is useful when you have created FileGroup objects using + New-SqlDscFileGroup and want to associate them with a Database. + + .PARAMETER Database + Specifies the Database object to which the FileGroups will be added. + + .PARAMETER FileGroup + Specifies one or more FileGroup objects to add to the Database. This parameter + accepts pipeline input. + + .PARAMETER PassThru + Returns the FileGroup objects that were added to the Database. + + .PARAMETER Force + Specifies that the FileGroup should be added without confirmation. + + .INPUTS + Microsoft.SqlServer.Management.Smo.FileGroup + + FileGroup objects that will be added to the Database. + + .OUTPUTS + None + + This cmdlet does not generate output by default. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.FileGroup[] + + When the PassThru parameter is specified, returns the FileGroup objects that were added. + + .EXAMPLE + Add-SqlDscFileGroup -Database $database -FileGroup $fileGroup + + Adds a single FileGroup to the Database. + + .EXAMPLE + $fileGroups | Add-SqlDscFileGroup -Database $database -PassThru + + Adds multiple FileGroups to the Database via pipeline and returns the FileGroup objects. +#> +function Add-SqlDscFileGroup +{ + [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.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([Microsoft.SqlServer.Management.Smo.FileGroup[]])] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.Database] + $Database, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [Microsoft.SqlServer.Management.Smo.FileGroup[]] + $FileGroup, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + foreach ($fileGroupObject in $FileGroup) + { + $descriptionMessage = $script:localizedData.AddSqlDscFileGroup_Add_ShouldProcessDescription -f $fileGroupObject.Name, $Database.Name + $confirmationMessage = $script:localizedData.AddSqlDscFileGroup_Add_ShouldProcessConfirmation -f $fileGroupObject.Name + $captionMessage = $script:localizedData.AddSqlDscFileGroup_Add_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + $Database.FileGroups.Add($fileGroupObject) + + if ($PassThru.IsPresent) + { + $fileGroupObject + } + } + } + } +} diff --git a/source/Public/ConvertTo-SqlDscDataFile.ps1 b/source/Public/ConvertTo-SqlDscDataFile.ps1 new file mode 100644 index 0000000000..b2a81e473c --- /dev/null +++ b/source/Public/ConvertTo-SqlDscDataFile.ps1 @@ -0,0 +1,80 @@ +<# + .SYNOPSIS + Converts a DatabaseFileSpec object to a SMO DataFile object. + + .DESCRIPTION + This command takes a DatabaseFileSpec specification object and converts it + to a SMO (SQL Server Management Objects) DataFile object. This is used + internally when creating databases with custom file configurations. + + .PARAMETER FileGroupObject + The SMO FileGroup object to which the DataFile will belong. + + .PARAMETER DataFileSpec + The DatabaseFileSpec object containing the data file configuration. + + .INPUTS + None + + This command does not accept pipeline input. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.DataFile + + Returns a SMO DataFile object bound to the provided FileGroup. + + .EXAMPLE + $fileSpec = New-SqlDscDataFile -Name 'TestDB_Data' -FileName 'C:\SQLData\TestDB.mdf' -AsSpec + $fileGroup = [Microsoft.SqlServer.Management.Smo.FileGroup]::new($database, 'PRIMARY') + $dataFile = ConvertTo-SqlDscDataFile -FileGroupObject $fileGroup -DataFileSpec $fileSpec + + Converts a DatabaseFileSpec to a SMO DataFile object. +#> +function ConvertTo-SqlDscDataFile +{ + [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.')] + [CmdletBinding()] + [OutputType([Microsoft.SqlServer.Management.Smo.DataFile])] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.FileGroup] + $FileGroupObject, + + [Parameter(Mandatory = $true)] + [DatabaseFileSpec] + $DataFileSpec + ) + + # Create SMO DataFile object + $smoDataFile = [Microsoft.SqlServer.Management.Smo.DataFile]::new($FileGroupObject, $DataFileSpec.Name) + $smoDataFile.FileName = $DataFileSpec.FileName + + # Set optional data file properties + if ($DataFileSpec.Size -gt 0) + { + $smoDataFile.Size = $DataFileSpec.Size + } + + if ($DataFileSpec.MaxSize -gt 0) + { + $smoDataFile.MaxSize = $DataFileSpec.MaxSize + } + + if ($DataFileSpec.Growth -gt 0) + { + $smoDataFile.Growth = $DataFileSpec.Growth + } + + if ($DataFileSpec.GrowthType) + { + $smoDataFile.GrowthType = $DataFileSpec.GrowthType + } + + if ($DataFileSpec.IsPrimaryFile) + { + $smoDataFile.IsPrimaryFile = $DataFileSpec.IsPrimaryFile + } + + return $smoDataFile +} diff --git a/source/Public/ConvertTo-SqlDscFileGroup.ps1 b/source/Public/ConvertTo-SqlDscFileGroup.ps1 new file mode 100644 index 0000000000..0e1de42bba --- /dev/null +++ b/source/Public/ConvertTo-SqlDscFileGroup.ps1 @@ -0,0 +1,68 @@ +<# + .SYNOPSIS + Converts a DatabaseFileGroupSpec object to a SMO FileGroup object. + + .DESCRIPTION + This command takes a DatabaseFileGroupSpec specification object and converts it + to a SMO (SQL Server Management Objects) FileGroup object with all configured + data files. This is used internally when creating databases with custom file + group configurations. + + .PARAMETER DatabaseObject + The SMO Database object to which the FileGroup will belong. + + .PARAMETER FileGroupSpec + The DatabaseFileGroupSpec object containing the file group configuration. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.FileGroup + + .EXAMPLE + $fileSpec = New-SqlDscDataFile -Name 'TestDB_Data' -FileName 'C:\SQLData\TestDB.mdf' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($fileSpec) -AsSpec + $smoFileGroup = ConvertTo-SqlDscFileGroup -DatabaseObject $database -FileGroupSpec $fileGroupSpec + + Converts a DatabaseFileGroupSpec to a SMO FileGroup object with data files. +#> +function ConvertTo-SqlDscFileGroup +{ + [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.')] + [CmdletBinding()] + [OutputType([Microsoft.SqlServer.Management.Smo.FileGroup])] + param + ( + [Parameter(Mandatory = $true)] + [Microsoft.SqlServer.Management.Smo.Database] + $DatabaseObject, + + [Parameter(Mandatory = $true)] + [DatabaseFileGroupSpec] + $FileGroupSpec + ) + + # Create SMO FileGroup object + $smoFileGroup = [Microsoft.SqlServer.Management.Smo.FileGroup]::new($DatabaseObject, $FileGroupSpec.Name) + + # Set file group properties + if ($null -ne $FileGroupSpec.ReadOnly) + { + $smoFileGroup.ReadOnly = $FileGroupSpec.ReadOnly + } + + if ($null -ne $FileGroupSpec.IsDefault) + { + $smoFileGroup.IsDefault = $FileGroupSpec.IsDefault + } + + # Add data files to the file group + if ($FileGroupSpec.Files -and $FileGroupSpec.Files.Count -gt 0) + { + foreach ($fileSpec in $FileGroupSpec.Files) + { + $smoDataFile = ConvertTo-SqlDscDataFile -FileGroupObject $smoFileGroup -DataFileSpec $fileSpec + $smoFileGroup.Files.Add($smoDataFile) + } + } + + return $smoFileGroup +} diff --git a/source/Public/New-SqlDscDataFile.ps1 b/source/Public/New-SqlDscDataFile.ps1 new file mode 100644 index 0000000000..3a4489b307 --- /dev/null +++ b/source/Public/New-SqlDscDataFile.ps1 @@ -0,0 +1,281 @@ +<# + .SYNOPSIS + Creates a new DataFile object for a SQL Server FileGroup and adds it to the FileGroup. + + .DESCRIPTION + This command creates a new DataFile object and automatically adds it to the specified + FileGroup's Files collection. The DataFile object represents a physical database file + (.mdf, .ndf, or .ss for snapshots). + + .PARAMETER FileGroup + Specifies the FileGroup object to which this DataFile will belong. The DataFile + will be automatically added to this FileGroup's Files collection. + + .PARAMETER Name + Specifies the logical name of the DataFile. + + .PARAMETER FileName + Specifies the physical path and filename for the DataFile. For database snapshots, + this should point to a sparse file location (typically with an .ss extension). + For regular databases, this should be the data file path (typically with .mdf + or .ndf extension). + + .PARAMETER DataFileSpec + Specifies a DatabaseFileSpec object that defines the data file configuration + including name, file path, size, growth, and other properties. + + .PARAMETER AsSpec + Returns a DatabaseFileSpec object instead of a SMO DataFile object. + This specification object can be used with New-SqlDscFileGroup -AsSpec + or passed directly to New-SqlDscDatabase to define data files before + the database is created. + + When this parameter is used, the command always returns a DatabaseFileSpec + object, regardless of the PassThru parameter, and the FileGroup parameter + is not available. + + .PARAMETER Size + Specifies the initial size of the data file in kilobytes. Only valid when + used with the -AsSpec parameter to create a DatabaseFileSpec object. + + .PARAMETER MaxSize + Specifies the maximum size to which the data file can grow, in kilobytes. + Only valid when used with the -AsSpec parameter to create a DatabaseFileSpec + object. + + .PARAMETER Growth + Specifies the amount by which the data file grows when it requires more space. + The value is interpreted according to the GrowthType parameter (kilobytes or + percentage). Only valid when used with the -AsSpec parameter to create a + DatabaseFileSpec object. + + .PARAMETER GrowthType + Specifies the type of growth for the data file. Valid values are 'KB', 'MB', + or 'Percent'. Only valid when used with the -AsSpec parameter to create a + DatabaseFileSpec object. + + .PARAMETER IsPrimaryFile + Specifies that this data file is the primary file in the PRIMARY filegroup. + Only valid when used with the -AsSpec parameter to create a DatabaseFileSpec + object. + + .PARAMETER PassThru + Returns the DataFile object that was created and added to the FileGroup. + Only available when using the Standard or FromSpec parameter sets. When + using the AsSpec parameter set, a DatabaseFileSpec object is always returned. + + .PARAMETER Force + Specifies that the DataFile object should be created without prompting for + confirmation. By default, the command prompts for confirmation when the FileGroup + parameter is provided. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $database = $serverObject.Databases['MyDatabase'] + $fileGroup = New-SqlDscFileGroup -Database $database -Name 'PRIMARY' + New-SqlDscDataFile -FileGroup $fileGroup -Name 'MyDatabase_Data' -FileName 'C:\Data\MyDatabase_Data.mdf' -Force + + Creates a new DataFile for a regular database with a FileGroup and adds it to the FileGroup. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $database = $serverObject.Databases['MyDatabase'] + $fileGroup = New-SqlDscFileGroup -Database $database -Name 'PRIMARY' + $dataFile = New-SqlDscDataFile -FileGroup $fileGroup -Name 'MySnapshot_Data' -FileName 'C:\Snapshots\MySnapshot_Data.ss' -PassThru -Force + + Creates a new sparse file for a database snapshot and returns the DataFile object. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $database = $serverObject.Databases['MyDatabase'] + $fileGroup = $database.FileGroups['PRIMARY'] + $dataFile = New-SqlDscDataFile -FileGroup $fileGroup -Name 'AdditionalData' -FileName 'C:\Data\AdditionalData.ndf' -PassThru -Force + + Creates an additional DataFile and returns it for further processing. + + .EXAMPLE + $dataFileSpec = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -AsSpec + + Creates a DatabaseFileSpec object that can be used with New-SqlDscFileGroup -AsSpec + or passed to New-SqlDscDatabase. + + .EXAMPLE + $dataFileSpec = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -Size 102400 -MaxSize 5242880 -Growth 10240 -GrowthType 'KB' -IsPrimaryFile -AsSpec + + Creates a DatabaseFileSpec object with all properties set directly via parameters. + + .INPUTS + None + + This cmdlet does not accept input from the pipeline. + + .OUTPUTS + None + + This cmdlet does not generate output by default when using Standard or FromSpec parameter sets without PassThru. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.DataFile + + When using the Standard or FromSpec parameter sets with the PassThru parameter. + + .OUTPUTS + DatabaseFileSpec + + When using the AsSpec parameter to create a specification object. +#> +function New-SqlDscDataFile +{ + [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.')] + [CmdletBinding(DefaultParameterSetName = 'Standard', SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([Microsoft.SqlServer.Management.Smo.DataFile])] + [OutputType([DatabaseFileSpec])] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] + [Parameter(Mandatory = $true, ParameterSetName = 'FromSpec')] + [Microsoft.SqlServer.Management.Smo.FileGroup] + $FileGroup, + + [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] + [Parameter(Mandatory = $true, ParameterSetName = 'AsSpec')] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'Standard')] + [Parameter(Mandatory = $true, ParameterSetName = 'AsSpec')] + [ValidateNotNullOrEmpty()] + [System.String] + $FileName, + + [Parameter(Mandatory = $true, ParameterSetName = 'FromSpec')] + [ValidateNotNull()] + [DatabaseFileSpec] + $DataFileSpec, + + [Parameter(Mandatory = $true, ParameterSetName = 'AsSpec')] + [System.Management.Automation.SwitchParameter] + $AsSpec, + + [Parameter(ParameterSetName = 'AsSpec')] + [System.Nullable[System.Double]] + $Size, + + [Parameter(ParameterSetName = 'AsSpec')] + [System.Nullable[System.Double]] + $MaxSize, + + [Parameter(ParameterSetName = 'AsSpec')] + [System.Nullable[System.Double]] + $Growth, + + [Parameter(ParameterSetName = 'AsSpec')] + [ValidateSet('KB', 'MB', 'Percent')] + [System.String] + $GrowthType, + + [Parameter(ParameterSetName = 'AsSpec')] + [System.Management.Automation.SwitchParameter] + $IsPrimaryFile, + + [Parameter(ParameterSetName = 'Standard')] + [Parameter(ParameterSetName = 'FromSpec')] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter(ParameterSetName = 'Standard')] + [Parameter(ParameterSetName = 'FromSpec')] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + if ($PSCmdlet.ParameterSetName -eq 'AsSpec') + { + $fileSpec = [DatabaseFileSpec]::new($Name, $FileName) + + if ($PSBoundParameters.ContainsKey('Size')) + { + $fileSpec.Size = $Size + } + + if ($PSBoundParameters.ContainsKey('MaxSize')) + { + $fileSpec.MaxSize = $MaxSize + } + + if ($PSBoundParameters.ContainsKey('Growth')) + { + $fileSpec.Growth = $Growth + } + + if ($PSBoundParameters.ContainsKey('GrowthType')) + { + $fileSpec.GrowthType = $GrowthType + } + + if ($IsPrimaryFile.IsPresent) + { + $fileSpec.IsPrimaryFile = $true + } + + return $fileSpec + } + + # Validate that primary files can only be in the PRIMARY filegroup + if ($PSCmdlet.ParameterSetName -in @('FromSpec')) + { + $isPrimary = $null -ne $DataFileSpec -and $DataFileSpec.IsPrimaryFile + + if ($isPrimary -and $FileGroup.Name -ne 'PRIMARY') + { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + ($script:localizedData.DataFile_PrimaryFileMustBeInPrimaryFileGroup), + 'NSDDF0003', + [System.Management.Automation.ErrorCategory]::InvalidArgument, + $FileGroup + ) + ) + } + } + + # Determine the data file name based on parameter set + $dataFileName = if ($PSCmdlet.ParameterSetName -eq 'FromSpec') + { + $DataFileSpec.Name + } + else + { + $Name + } + + $descriptionMessage = $script:localizedData.DataFile_Create_ShouldProcessDescription -f $dataFileName, $FileGroup.Name + $confirmationMessage = $script:localizedData.DataFile_Create_ShouldProcessConfirmation -f $dataFileName + $captionMessage = $script:localizedData.DataFile_Create_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + if ($PSCmdlet.ParameterSetName -eq 'FromSpec') + { + # Convert the spec object to SMO DataFile + $dataFileObject = ConvertTo-SqlDscDataFile -FileGroupObject $FileGroup -DataFileSpec $DataFileSpec + } + else + { + $dataFileObject = [Microsoft.SqlServer.Management.Smo.DataFile]::new($FileGroup, $Name, $FileName) + } + + $FileGroup.Files.Add($dataFileObject) + + if ($PassThru.IsPresent) + { + return $dataFileObject + } + } +} diff --git a/source/Public/New-SqlDscDatabase.ps1 b/source/Public/New-SqlDscDatabase.ps1 index e070968673..59d234f6df 100644 --- a/source/Public/New-SqlDscDatabase.ps1 +++ b/source/Public/New-SqlDscDatabase.ps1 @@ -38,6 +38,19 @@ When this parameter is specified, a database snapshot will be created instead of a regular database. The snapshot name is specified in the Name parameter. + .PARAMETER FileGroup + Specifies an array of DatabaseFileGroupSpec objects that define the file groups + and data files for the database. Each DatabaseFileGroupSpec contains the file group + name and an array of DatabaseFileSpec objects for the data files. + + This parameter allows you to specify custom file and filegroup configurations + before the database is created, avoiding the SMO limitation where DataFile objects + require an existing database context. + + For database snapshots, the FileName in each DatabaseFileSpec must point to sparse + file locations. For regular databases, this allows full control over PRIMARY and + secondary file group configurations. + .PARAMETER Force Specifies that the database should be created without any confirmation. @@ -68,6 +81,21 @@ Creates a database snapshot named **MyDatabaseSnapshot** from the source database **MyDatabase** without prompting for confirmation. + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + + $primaryFile = New-SqlDscDataFile -Name 'MyDatabase_Primary' -FileName 'D:\SQLData\MyDatabase.mdf' -Size 102400 -Growth 10240 -GrowthType 'KB' -IsPrimaryFile -AsSpec + $primaryFileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile) -IsDefault $true -AsSpec + + $secondaryFile = New-SqlDscDataFile -Name 'MyDatabase_Secondary' -FileName 'E:\SQLData\MyDatabase.ndf' -Size 204800 -AsSpec + $secondaryFileGroup = New-SqlDscFileGroup -Name 'SECONDARY' -Files @($secondaryFile) -AsSpec + + $serverObject | New-SqlDscDatabase -Name 'MyDatabase' -FileGroup @($primaryFileGroup, $secondaryFileGroup) -Force + + Creates a new database named **MyDatabase** with custom PRIMARY and SECONDARY file groups + using specification objects created with the -AsSpec parameter. All properties are set + directly via parameters without prompting for confirmation. + .OUTPUTS `[Microsoft.SqlServer.Management.Smo.Database]` #> @@ -115,6 +143,10 @@ function New-SqlDscDatabase [System.String] $DatabaseSnapshotBaseName, + [Parameter()] + [DatabaseFileGroupSpec[]] + $FileGroup, + [Parameter()] [System.Management.Automation.SwitchParameter] $Force, @@ -282,6 +314,19 @@ function New-SqlDscDatabase } } + # Add FileGroups if provided (applies to both regular databases and snapshots) + if ($PSBoundParameters.ContainsKey('FileGroup')) + { + foreach ($fileGroupSpec in $FileGroup) + { + # Create FileGroup using New-SqlDscFileGroup with spec object + $smoFileGroup = New-SqlDscFileGroup -Database $sqlDatabaseObjectToCreate -FileGroupSpec $fileGroupSpec -Force + + # Add the file group to the database + Add-SqlDscFileGroup -Database $sqlDatabaseObjectToCreate -FileGroup $smoFileGroup -Force + } + } + Write-Verbose -Message ($script:localizedData.Database_Creating -f $Name) $sqlDatabaseObjectToCreate.Create() diff --git a/source/Public/New-SqlDscDatabaseSnapshot.ps1 b/source/Public/New-SqlDscDatabaseSnapshot.ps1 new file mode 100644 index 0000000000..5dff09929f --- /dev/null +++ b/source/Public/New-SqlDscDatabaseSnapshot.ps1 @@ -0,0 +1,269 @@ +<# + .SYNOPSIS + Creates a new database snapshot in a SQL Server Database Engine instance. + + .DESCRIPTION + This command creates a new database snapshot in a SQL Server Database Engine + instance using SMO. It provides an automated and DSC-friendly approach to + snapshot management by leveraging the existing `New-SqlDscDatabase` command + for the actual creation. + + .PARAMETER ServerObject + Specifies the current server connection object. This parameter is used in the + ServerObject parameter set. + + .PARAMETER Name + Specifies the name of the database snapshot to be created. + + .PARAMETER DatabaseName + Specifies the name of the source database from which to create a snapshot. + This parameter is used in the ServerObject parameter set. + + .PARAMETER DatabaseObject + Specifies the source database object to snapshot. This parameter can be + provided via pipeline and is used in the DatabaseObject parameter set. + + .PARAMETER FileGroup + Specifies an array of DatabaseFileGroupSpec objects that define the file groups + and data files for the database snapshot. Each DatabaseFileGroupSpec contains the + file group name and an array of DatabaseFileSpec objects for the sparse files. + When not specified, the cmdlet automatically generates DatabaseFileGroupSpec + and DatabaseFileSpec entries based on the source database's file structure, + resulting in sparse files being created in the default data directory with + automatically generated names. + + .PARAMETER Force + Specifies that the snapshot should be created without any confirmation. + + .PARAMETER Refresh + Specifies that the **ServerObject**'s databases should be refreshed before + creating the snapshot. 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-SqlDscDatabaseSnapshot -Name 'MyDatabase_Snapshot' -DatabaseName 'MyDatabase' + + Creates a new database snapshot named **MyDatabase_Snapshot** from the source + database **MyDatabase**. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $databaseObject = $serverObject | Get-SqlDscDatabase -Name 'MyDatabase' + $databaseObject | New-SqlDscDatabaseSnapshot -Name 'MyDatabase_Snapshot' -Force + + Creates a new database snapshot named **MyDatabase_Snapshot** from the database + object **MyDatabase** without prompting for confirmation. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $serverObject | New-SqlDscDatabaseSnapshot -Name 'MyDB_Snap' -DatabaseName 'MyDatabase' -Force + + Creates a new database snapshot named **MyDB_Snap** from the source database + **MyDatabase** without prompting for confirmation. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $sourceDb = $serverObject.Databases['MyDatabase'] + + $dataFile = New-SqlDscDataFile -Name 'MyDatabase_Data' -FileName 'C:\Snapshots\MyDatabase_Data.ss' -AsSpec + $fileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($dataFile) -AsSpec + + $serverObject | New-SqlDscDatabaseSnapshot -Name 'MyDB_Snap' -DatabaseName 'MyDatabase' -FileGroup @($fileGroup) -Force + + Creates a new database snapshot named **MyDB_Snap** from the source database + **MyDatabase** with a specified sparse file location without prompting for confirmation. + + .INPUTS + `[Microsoft.SqlServer.Management.Smo.Server]` + + Specifies the SQL Server connection object to create the snapshot in. + + .INPUTS + `[Microsoft.SqlServer.Management.Smo.Database]` + + Specifies the source database object to create a snapshot from. + + .OUTPUTS + `[Microsoft.SqlServer.Management.Smo.Database]` + + Returns the newly created database snapshot object. + + .NOTES + This command is for snapshot creation only and does not support modification + of existing snapshots. + + Database snapshots are only supported in certain SQL Server editions (Enterprise, + Developer, and Evaluation editions). The command will validate edition support + before attempting to create the snapshot. +#> +function New-SqlDscDatabaseSnapshot +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSShouldProcess', '', Justification = 'Because ShouldProcess is used in 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(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(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(ParameterSetName = 'ServerObject', Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [System.String] + $DatabaseName, + + [Parameter()] + [DatabaseFileGroupSpec[]] + $FileGroup, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter(ParameterSetName = 'ServerObject')] + [System.Management.Automation.SwitchParameter] + $Refresh + ) + + begin + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + } + + process + { + # Determine the server object and source database name based on parameter set + if ($PSCmdlet.ParameterSetName -eq 'DatabaseObject') + { + $ServerObject = $DatabaseObject.Parent + $DatabaseName = $DatabaseObject.Name + } + + # Validate SQL Server edition supports snapshots + $supportedEditions = @('Enterprise', 'Developer', 'EnterpriseCore', 'EnterpriseOrDeveloper') + + if ($ServerObject.EngineEdition -notin @('Enterprise', 'EnterpriseEvaluation')) + { + # Check edition name for older servers or evaluation + $editionName = $ServerObject.Edition + + $isSupported = $false + + foreach ($supportedEdition in $supportedEditions) + { + if ($editionName -like "*$supportedEdition*") + { + $isSupported = $true + break + } + } + + # Also check for Evaluation edition + if ($editionName -like '*Evaluation*') + { + $isSupported = $true + } + + if (-not $isSupported) + { + $errorMessage = $script:localizedData.DatabaseSnapshot_EditionNotSupported -f $ServerObject.InstanceName, $editionName + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.InvalidOperationException]::new($errorMessage), + 'NSDS0001', # cspell: disable-line + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $ServerObject + ) + ) + } + } + + Write-Verbose -Message ($script:localizedData.DatabaseSnapshot_Create -f $Name, $DatabaseName, $ServerObject.InstanceName) + + # If FileGroup is not specified, automatically create file groups based on source database + if (-not $PSBoundParameters.ContainsKey('FileGroup')) + { + # Get the source database object + $getSqlDscDatabaseParameters = @{ + ServerObject = $ServerObject + Name = $DatabaseName + ErrorAction = 'Stop' + } + + if ($PSCmdlet.ParameterSetName -eq 'ServerObject' -and $Refresh.IsPresent) + { + $getSqlDscDatabaseParameters['Refresh'] = $true + } + + $sourceDatabase = Get-SqlDscDatabase @getSqlDscDatabaseParameters + + # Get the default data directory for sparse files + $defaultDataDirectory = $ServerObject.Settings.DefaultFile + + if (-not $defaultDataDirectory) + { + $defaultDataDirectory = $ServerObject.Information.MasterDBPath + } + + # Create file group specifications for all file groups in the source database + $generatedFileGroups = [System.Collections.Generic.List[DatabaseFileGroupSpec]]::new() + + foreach ($sourceFileGroup in $sourceDatabase.FileGroups) + { + $fileSpecs = [System.Collections.Generic.List[DatabaseFileSpec]]::new() + + foreach ($sourceFile in $sourceFileGroup.Files) + { + # Use the same physical filename as the source file, but with .ss extension + $sourceFileName = [System.IO.Path]::GetFileNameWithoutExtension($sourceFile.FileName) + $sparseFileName = '{0}.ss' -f $sourceFileName + $sparseFilePath = Join-Path -Path $defaultDataDirectory -ChildPath $sparseFileName + + # Create a file spec using the same logical name as the source database file + $fileSpecs.Add((New-SqlDscDataFile -Name $sourceFile.Name -FileName $sparseFilePath -AsSpec)) + } + + # Create file group spec + $generatedFileGroups.Add((New-SqlDscFileGroup -Name $sourceFileGroup.Name -Files $fileSpecs.ToArray() -AsSpec)) + } + + $FileGroup = $generatedFileGroups.ToArray() + } + + # Create the snapshot using New-SqlDscDatabase + $newSqlDscDatabaseParameters = @{ + ServerObject = $ServerObject + Name = $Name + DatabaseSnapshotBaseName = $DatabaseName + FileGroup = $FileGroup + Force = $Force + WhatIf = $WhatIfPreference + } + + if ($PSCmdlet.ParameterSetName -eq 'ServerObject' -and $Refresh.IsPresent) + { + $newSqlDscDatabaseParameters['Refresh'] = $true + } + + $snapshotDatabaseObject = New-SqlDscDatabase @newSqlDscDatabaseParameters + + return $snapshotDatabaseObject + } +} diff --git a/source/Public/New-SqlDscFileGroup.ps1 b/source/Public/New-SqlDscFileGroup.ps1 new file mode 100644 index 0000000000..950eab88f4 --- /dev/null +++ b/source/Public/New-SqlDscFileGroup.ps1 @@ -0,0 +1,218 @@ +<# + .SYNOPSIS + Creates a new FileGroup object for a SQL Server database. + + .DESCRIPTION + This command creates a new FileGroup object that can be used when creating + or modifying SQL Server databases. The FileGroup object can contain DataFile + objects. The FileGroup can be created with or without an associated Database, + allowing it to be added to a Database later using Add-SqlDscFileGroup. + + .PARAMETER Database + Specifies the Database object to which this FileGroup will belong. This parameter + is optional. If not specified, a standalone FileGroup is created that can be + added to a Database later. + + .PARAMETER Name + Specifies the name of the FileGroup to create. + + .PARAMETER FileGroupSpec + Specifies a DatabaseFileGroupSpec object that defines the file group configuration + including name, properties, and data files. + + .PARAMETER AsSpec + Returns a DatabaseFileGroupSpec object instead of a SMO FileGroup object. + This specification object can be passed to New-SqlDscDatabase to define + file groups before the database is created. + + .PARAMETER Files + Specifies an array of DatabaseFileSpec objects to include in the file group. + Only valid when using -AsSpec. + + .PARAMETER ReadOnly + Specifies whether the file group should be read-only. Only valid when using -AsSpec. + + .PARAMETER IsDefault + Specifies whether this file group should be the default file group. Only valid when using -AsSpec. + + .PARAMETER Force + Specifies that the FileGroup object should be created without prompting for + confirmation. By default, the command prompts for confirmation when the Database + parameter is provided. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $database = $serverObject.Databases['MyDatabase'] + $fileGroup = New-SqlDscFileGroup -Database $database -Name 'MyFileGroup' + + Creates a new FileGroup named 'MyFileGroup' for the specified database. + + .EXAMPLE + $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' + $database = $serverObject.Databases['MyDatabase'] + $fileGroup = New-SqlDscFileGroup -Database $database -Name 'PRIMARY' + + Creates a new PRIMARY FileGroup for the specified database. + + .EXAMPLE + $fileGroup = New-SqlDscFileGroup -Name 'MyFileGroup' + # Later add to database + Add-SqlDscFileGroup -Database $database -FileGroup $fileGroup + + Creates a standalone FileGroup that can be added to a Database later. + + .EXAMPLE + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -AsSpec + + Creates a DatabaseFileGroupSpec object that can be passed to New-SqlDscDatabase. + + .EXAMPLE + $primaryFile = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -Size 102400 -IsPrimaryFile -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile) -IsDefault $true -AsSpec + + Creates a DatabaseFileGroupSpec object with files and properties set directly via parameters. + + .INPUTS + None + + This cmdlet does not accept input from the pipeline. + + .OUTPUTS + Microsoft.SqlServer.Management.Smo.FileGroup + + When creating a FileGroup with or without an associated Database (not using -AsSpec). + + .OUTPUTS + DatabaseFileGroupSpec + + When using the -AsSpec parameter to create a specification object. +#> +function New-SqlDscFileGroup +{ + [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.')] + [CmdletBinding(DefaultParameterSetName = 'Standalone', SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([Microsoft.SqlServer.Management.Smo.FileGroup])] + [OutputType([DatabaseFileGroupSpec])] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'WithDatabase')] + [Parameter(Mandatory = $true, ParameterSetName = 'WithDatabaseFromSpec')] + [Microsoft.SqlServer.Management.Smo.Database] + $Database, + + [Parameter(Mandatory = $true, ParameterSetName = 'WithDatabase')] + [Parameter(Mandatory = $true, ParameterSetName = 'Standalone')] + [Parameter(Mandatory = $true, ParameterSetName = 'AsSpec')] + [ValidateNotNullOrEmpty()] + [System.String] + $Name, + + [Parameter(Mandatory = $true, ParameterSetName = 'WithDatabaseFromSpec')] + [DatabaseFileGroupSpec] + $FileGroupSpec, + + [Parameter(Mandatory = $true, ParameterSetName = 'AsSpec')] + [System.Management.Automation.SwitchParameter] + $AsSpec, + + [Parameter(ParameterSetName = 'AsSpec')] + [DatabaseFileSpec[]] + $Files, + + [Parameter(ParameterSetName = 'AsSpec')] + [System.Management.Automation.SwitchParameter] + $ReadOnly, + + [Parameter(ParameterSetName = 'AsSpec')] + [System.Management.Automation.SwitchParameter] + $IsDefault, + + [Parameter(ParameterSetName = 'WithDatabase')] + [Parameter(ParameterSetName = 'WithDatabaseFromSpec')] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $fileGroupObject = $null + + if ($PSCmdlet.ParameterSetName -in @('WithDatabase', 'WithDatabaseFromSpec')) + { + if (-not $Database.Parent) + { + $errorMessage = $script:localizedData.FileGroup_DatabaseMissingServerObject + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + $errorMessage, + 'NSDFG0003', + [System.Management.Automation.ErrorCategory]::InvalidArgument, + $Database + ) + ) + } + + $serverObject = $Database.Parent + + # Determine the file group name based on parameter set + $fileGroupName = if ($PSCmdlet.ParameterSetName -eq 'WithDatabaseFromSpec') + { + $FileGroupSpec.Name + } + else + { + $Name + } + + $descriptionMessage = $script:localizedData.FileGroup_Create_ShouldProcessDescription -f $fileGroupName, $Database.Name, $serverObject.InstanceName + $confirmationMessage = $script:localizedData.FileGroup_Create_ShouldProcessConfirmation -f $fileGroupName + $captionMessage = $script:localizedData.FileGroup_Create_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + if ($PSCmdlet.ParameterSetName -eq 'WithDatabaseFromSpec') + { + # Convert the spec object to SMO FileGroup + $fileGroupObject = ConvertTo-SqlDscFileGroup -DatabaseObject $Database -FileGroupSpec $FileGroupSpec + } + else + { + $fileGroupObject = [Microsoft.SqlServer.Management.Smo.FileGroup]::new($Database, $Name) + } + } + } + else + { + if ($AsSpec.IsPresent) + { + $fileGroupObject = [DatabaseFileGroupSpec]::new($Name) + + if ($PSBoundParameters.ContainsKey('Files')) + { + $fileGroupObject.Files = $Files + } + + if ($ReadOnly.IsPresent) + { + $fileGroupObject.ReadOnly = $true + } + + if ($IsDefault.IsPresent) + { + $fileGroupObject.IsDefault = $true + } + } + else + { + $fileGroupObject = [Microsoft.SqlServer.Management.Smo.FileGroup]::new() + + $fileGroupObject.Name = $Name + } + } + + return $fileGroupObject +} diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index af4fbf7ff8..3787af4c82 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -365,6 +365,30 @@ ConvertFrom-StringData @' # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. Database_Create_ShouldProcessCaption = Create database on instance + ## New-SqlDscDatabaseSnapshot + DatabaseSnapshot_Create = Creating database snapshot '{0}' from source database '{1}' on instance '{2}'. (NSDS0002) + DatabaseSnapshot_EditionNotSupported = Database snapshots are not supported on SQL Server instance '{0}' with edition '{1}'. Snapshots are only supported in Enterprise, Developer, and Evaluation editions. (NSDS0001) + + ## New-SqlDscFileGroup + FileGroup_Create_ShouldProcessDescription = Creating the filegroup '{0}' for database '{1}' on instance '{2}'. (NSDFG0001) + FileGroup_Create_ShouldProcessConfirmation = Are you sure you want to create the filegroup '{0}'? (NSDFG0002) + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + FileGroup_Create_ShouldProcessCaption = Create filegroup for database + FileGroup_DatabaseMissingServerObject = The Database object must have a Server object attached to the Parent property. (NSDFG0003) + + ## Add-SqlDscFileGroup + AddSqlDscFileGroup_Add_ShouldProcessDescription = Adding the filegroup '{0}' to database '{1}'. (ASDFG0001) + AddSqlDscFileGroup_Add_ShouldProcessConfirmation = Are you sure you want to add the filegroup '{0}'? (ASDFG0002) + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + AddSqlDscFileGroup_Add_ShouldProcessCaption = Add filegroup to database + + ## New-SqlDscDataFile + DataFile_Create_ShouldProcessDescription = Creating the data file '{0}' for filegroup '{1}'. (NSDDF0001) + DataFile_Create_ShouldProcessConfirmation = Are you sure you want to create the data file '{0}'? (NSDDF0002) + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + DataFile_Create_ShouldProcessCaption = Create data file for filegroup + DataFile_PrimaryFileMustBeInPrimaryFileGroup = The primary file must reside in the PRIMARY filegroup. (NSDDF0003) + ## Set-SqlDscDatabaseProperty Database_Set = Setting properties of database '{0}' on instance '{1}'. (SSDDP0001) Database_Updating = Updating database '{0}'. (SSDDP0002) diff --git a/tests/Integration/Commands/Add-SqlDscFileGroup.Integration.Tests.ps1 b/tests/Integration/Commands/Add-SqlDscFileGroup.Integration.Tests.ps1 new file mode 100644 index 0000000000..5b18b2fbc7 --- /dev/null +++ b/tests/Integration/Commands/Add-SqlDscFileGroup.Integration.Tests.ps1 @@ -0,0 +1,153 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' +} + +Describe 'Add-SqlDscFileGroup' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $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 + } + + Context 'When adding a FileGroup to a Database with real SMO types' { + BeforeEach { + # Create real SMO Database object + $script:testDatabase = [Microsoft.SqlServer.Management.Smo.Database]::new() + $script:testDatabase.Name = 'TestDatabase' + $script:testDatabase.Parent = $script:serverObject + + $script:testFileGroup = New-SqlDscFileGroup -Database $script:testDatabase -Name 'TestFileGroup' -Confirm:$false -ErrorAction 'Stop' + } + + It 'Should add a FileGroup to Database successfully' { + $initialCount = $script:testDatabase.FileGroups.Count + + Add-SqlDscFileGroup -Database $script:testDatabase -FileGroup $script:testFileGroup -ErrorAction 'Stop' + + $script:testDatabase.FileGroups.Count | Should -Be ($initialCount + 1) + $script:testDatabase.FileGroups[$script:testFileGroup.Name] | Should -Be $script:testFileGroup + } + + It 'Should return FileGroup when using PassThru' { + $result = Add-SqlDscFileGroup -Database $script:testDatabase -FileGroup $script:testFileGroup -PassThru -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result | Should -Be $script:testFileGroup + } + + It 'Should accept FileGroup from pipeline' { + $initialCount = $script:testDatabase.FileGroups.Count + + $script:testFileGroup | Add-SqlDscFileGroup -Database $script:testDatabase -ErrorAction 'Stop' + + $script:testDatabase.FileGroups.Count | Should -Be ($initialCount + 1) + } + + It 'Should add multiple FileGroups to Database' { + $fileGroup1 = New-SqlDscFileGroup -Database $script:testDatabase -Name 'FileGroup1' -Confirm:$false -ErrorAction 'Stop' + $fileGroup2 = New-SqlDscFileGroup -Database $script:testDatabase -Name 'FileGroup2' -Confirm:$false -ErrorAction 'Stop' + + $initialCount = $script:testDatabase.FileGroups.Count + + Add-SqlDscFileGroup -Database $script:testDatabase -FileGroup @($fileGroup1, $fileGroup2) -ErrorAction 'Stop' + + $script:testDatabase.FileGroups.Count | Should -Be ($initialCount + 2) + $script:testDatabase.FileGroups[$fileGroup1.Name] | Should -Be $fileGroup1 + $script:testDatabase.FileGroups[$fileGroup2.Name] | Should -Be $fileGroup2 + } + + It 'Should add multiple FileGroups via pipeline and return them with PassThru' { + $fileGroup1 = New-SqlDscFileGroup -Database $script:testDatabase -Name 'FileGroup1' -Confirm:$false -ErrorAction 'Stop' + $fileGroup2 = New-SqlDscFileGroup -Database $script:testDatabase -Name 'FileGroup2' -Confirm:$false -ErrorAction 'Stop' + + $result = @($fileGroup1, $fileGroup2) | Add-SqlDscFileGroup -Database $script:testDatabase -PassThru -ErrorAction 'Stop' + + $result | Should -HaveCount 2 + $result[0] | Should -Be $fileGroup1 + $result[1] | Should -Be $fileGroup2 + } + } + + Context 'When verifying FileGroup parent relationship' { + BeforeEach { + $script:testDatabase = [Microsoft.SqlServer.Management.Smo.Database]::new() + $script:testDatabase.Name = 'TestDatabase' + $script:testDatabase.Parent = $script:serverObject + + $script:testFileGroup = New-SqlDscFileGroup -Name 'TestFileGroup' -ErrorAction 'Stop' + } + + It 'Should update FileGroup parent reference when added to Database' { + $script:testFileGroup.Parent | Should -BeNullOrEmpty + + Add-SqlDscFileGroup -Database $script:testDatabase -FileGroup $script:testFileGroup -ErrorAction 'Stop' + + # Note: The parent may or may not be updated depending on SMO implementation + # This test verifies the FileGroup is in the collection + $script:testDatabase.FileGroups[$script:testFileGroup.Name] | Should -Be $script:testFileGroup + } + } + + Context 'When integrating FileGroup and DataFile creation' { + BeforeEach { + $script:testDatabase = [Microsoft.SqlServer.Management.Smo.Database]::new() + $script:testDatabase.Name = 'TestDatabase' + $script:testDatabase.Parent = $script:serverObject + } + + It 'Should create a complete FileGroup with DataFile structure' { + # Create FileGroup + $fileGroup = New-SqlDscFileGroup -Database $script:testDatabase -Name 'SecondaryFileGroup' -Confirm:$false -ErrorAction 'Stop' + + # Create DataFile - it will be automatically added to the FileGroup + $null = New-SqlDscDataFile -FileGroup $fileGroup -Name 'SecondaryDataFile' -FileName 'C:\Data\SecondaryDataFile.ndf' -Confirm:$false -ErrorAction 'Stop' + + # Add FileGroup to Database + Add-SqlDscFileGroup -Database $script:testDatabase -FileGroup $fileGroup -ErrorAction 'Stop' + + # Verify structure + $script:testDatabase.FileGroups[$fileGroup.Name] | Should -Be $fileGroup + $addedFile = $script:testDatabase.FileGroups[$fileGroup.Name].Files | Where-Object -FilterScript { $_.Name -eq 'SecondaryDataFile' } + $addedFile | Should -Not -BeNullOrEmpty + $addedFile.FileName | Should -Be 'C:\Data\SecondaryDataFile.ndf' + } + } +} diff --git a/tests/Integration/Commands/ConvertTo-SqlDscDataFile.Integration.Tests.ps1 b/tests/Integration/Commands/ConvertTo-SqlDscDataFile.Integration.Tests.ps1 new file mode 100644 index 0000000000..806959bef2 --- /dev/null +++ b/tests/Integration/Commands/ConvertTo-SqlDscDataFile.Integration.Tests.ps1 @@ -0,0 +1,146 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + # Import the SMO module to ensure real SMO types are available + Import-SqlDscPreferredModule +} + +Describe 'ConvertTo-SqlDscDataFile' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $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 + } + + Context 'When converting a DatabaseFileSpec to a DataFile with real SMO types' { + BeforeAll { + $script:testDatabaseName = 'TestDB_{0}' -f (Get-Random) + + # Create a test database for the file group context + $script:testDatabase = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Confirm:$false + } + + AfterAll { + # Clean up the test database + if ($script:testDatabase) + { + Remove-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Confirm:$false + } + } + + BeforeEach { + $script:mockFileGroup = New-SqlDscFileGroup -Database $script:testDatabase -Name 'TestFileGroup' -Confirm:$false + } + + It 'Should convert a minimal DatabaseFileSpec to a DataFile object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $script:mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.DataFile] + $result.Name | Should -Be 'TestFile' + $result.FileName | Should -Be 'C:\Data\TestFile.ndf' + } + + It 'Should convert a DatabaseFileSpec with Size to a DataFile object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -Size 100 -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $script:mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.Size | Should -Be 100 + } + + It 'Should convert a DatabaseFileSpec with MaxSize to a DataFile object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -MaxSize 1000 -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $script:mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.MaxSize | Should -Be 1000 + } + + It 'Should convert a DatabaseFileSpec with Growth to a DataFile object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -Growth 10 -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $script:mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.Growth | Should -Be 10 + } + + It 'Should convert a DatabaseFileSpec with GrowthType to a DataFile object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -GrowthType 'Percent' -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $script:mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.GrowthType | Should -Be 'Percent' + } + + It 'Should convert a DatabaseFileSpec with IsPrimaryFile to a DataFile object' { + # IsPrimaryFile can only be set for files in the PRIMARY filegroup + $primaryFileGroup = $script:testDatabase.FileGroups['PRIMARY'] + + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.mdf' -IsPrimaryFile -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $primaryFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.IsPrimaryFile | Should -Be $true + } + + It 'Should convert a DatabaseFileSpec with all optional properties to a DataFile object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -Size 100 -MaxSize 1000 -Growth 10 -GrowthType 'Percent' -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $script:mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestFile' + $result.FileName | Should -Be 'C:\Data\TestFile.ndf' + $result.Size | Should -Be 100 + $result.MaxSize | Should -Be 1000 + $result.Growth | Should -Be 10 + $result.GrowthType | Should -Be 'Percent' + } + + } +} diff --git a/tests/Integration/Commands/ConvertTo-SqlDscFileGroup.Integration.Tests.ps1 b/tests/Integration/Commands/ConvertTo-SqlDscFileGroup.Integration.Tests.ps1 new file mode 100644 index 0000000000..a2eb67958a --- /dev/null +++ b/tests/Integration/Commands/ConvertTo-SqlDscFileGroup.Integration.Tests.ps1 @@ -0,0 +1,156 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + # Import the SMO module to ensure real SMO types are available + Import-SqlDscPreferredModule +} + +Describe 'ConvertTo-SqlDscFileGroup' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $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 + } + + Context 'When converting a DatabaseFileGroupSpec to a FileGroup with real SMO types' { + BeforeAll { + $script:testDatabaseName = 'TestDB_{0}' -f (Get-Random) + + # Create a test database for the conversion context + $script:testDatabase = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Confirm:$false + } + + AfterAll { + # Clean up the test database + if ($script:testDatabase) + { + Remove-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseName -Confirm:$false + } + } + + It 'Should convert a minimal DatabaseFileGroupSpec to a FileGroup object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'TestFileGroup' -Files @($fileSpec) -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $script:testDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType [Microsoft.SqlServer.Management.Smo.FileGroup] + $result.Name | Should -Be 'TestFileGroup' + $result.Files.Count | Should -Be 1 + $result.Files[0].Name | Should -Be 'TestFile' + } + + It 'Should convert a DatabaseFileGroupSpec with multiple files to a FileGroup object' { + $fileSpec1 = New-SqlDscDataFile -Name 'TestFile1' -FileName 'C:\Data\TestFile1.ndf' -AsSpec + $fileSpec2 = New-SqlDscDataFile -Name 'TestFile2' -FileName 'C:\Data\TestFile2.ndf' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'TestFileGroup' -Files @($fileSpec1, $fileSpec2) -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $script:testDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result.Files.Count | Should -Be 2 + $result.Files[0].Name | Should -Be 'TestFile1' + $result.Files[1].Name | Should -Be 'TestFile2' + } + + It 'Should convert a DatabaseFileGroupSpec with ReadOnly to a FileGroup object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'TestFileGroup' -Files @($fileSpec) -ReadOnly -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $script:testDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result.ReadOnly | Should -Be $true + } + + It 'Should convert a DatabaseFileGroupSpec with IsDefault to a FileGroup object' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\Data\TestFile.ndf' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'TestFileGroup' -Files @($fileSpec) -IsDefault -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $script:testDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result.IsDefault | Should -Be $true + } + + It 'Should convert a DatabaseFileGroupSpec with multiple file properties to a FileGroup object' { + $fileSpec1 = New-SqlDscDataFile -Name 'TestFile1' -FileName 'C:\Data\TestFile1.ndf' -Size 100 -MaxSize 1000 -AsSpec + $fileSpec2 = New-SqlDscDataFile -Name 'TestFile2' -FileName 'C:\Data\TestFile2.ndf' -Growth 10 -GrowthType 'Percent' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'TestFileGroup' -Files @($fileSpec1, $fileSpec2) -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $script:testDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestFileGroup' + $result.Files.Count | Should -Be 2 + $result.Files[0].Size | Should -Be 100 + $result.Files[0].MaxSize | Should -Be 1000 + $result.Files[1].Growth | Should -Be 10 + $result.Files[1].GrowthType | Should -Be 'Percent' + } + + It 'Should preserve file properties when converting FileGroup with complex file configurations' { + $primaryFile = New-SqlDscDataFile -Name 'PrimaryFile' -FileName 'C:\Data\Primary.mdf' -IsPrimaryFile -Size 200 -MaxSize 2000 -Growth 20 -GrowthType 'KB' -AsSpec + $secondaryFile = New-SqlDscDataFile -Name 'SecondaryFile' -FileName 'C:\Data\Secondary.ndf' -Size 100 -MaxSize 1000 -Growth 10 -GrowthType 'Percent' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile, $secondaryFile) -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $script:testDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result.Files.Count | Should -Be 2 + + # Verify primary file properties + $result.Files[0].Name | Should -Be 'PrimaryFile' + $result.Files[0].IsPrimaryFile | Should -Be $true + $result.Files[0].Size | Should -Be 200 + $result.Files[0].MaxSize | Should -Be 2000 + $result.Files[0].Growth | Should -Be 20 + $result.Files[0].GrowthType | Should -Be 'KB' + + # Verify secondary file properties + $result.Files[1].Name | Should -Be 'SecondaryFile' + $result.Files[1].Size | Should -Be 100 + $result.Files[1].MaxSize | Should -Be 1000 + $result.Files[1].Growth | Should -Be 10 + $result.Files[1].GrowthType | Should -Be 'Percent' + } + } +} diff --git a/tests/Integration/Commands/New-SqlDscDataFile.Integration.Tests.ps1 b/tests/Integration/Commands/New-SqlDscDataFile.Integration.Tests.ps1 new file mode 100644 index 0000000000..be4291f657 --- /dev/null +++ b/tests/Integration/Commands/New-SqlDscDataFile.Integration.Tests.ps1 @@ -0,0 +1,141 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + # Import the SMO module to ensure real SMO types are available + Import-SqlDscPreferredModule +} + +Describe 'New-SqlDscDataFile' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $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 + } + + Context 'When creating a DataFile with a FileGroup object' { + BeforeAll { + # Create a real SMO Database object + $script:mockDatabase = [Microsoft.SqlServer.Management.Smo.Database]::new() + $script:mockDatabase.Name = 'TestDatabase' + $script:mockDatabase.Parent = $script:serverObject + + $script:mockFileGroup = New-SqlDscFileGroup -Database $script:mockDatabase -Name 'TestFileGroup' -Force -ErrorAction 'Stop' + } + + It 'Should create a DataFile and add it to FileGroup without PassThru' { + $initialFileCount = $script:mockFileGroup.Files.Count + + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'TestDataFile' -FileName 'C:\Data\TestDataFile.ndf' -Force -ErrorAction 'Stop' + + $result | Should -BeNullOrEmpty + $script:mockFileGroup.Files.Count | Should -Be ($initialFileCount + 1) + + $addedFile = $script:mockFileGroup.Files | Where-Object -FilterScript { $_.Name -eq 'TestDataFile' } + $addedFile | Should -Not -BeNullOrEmpty + $addedFile.FileName | Should -Be 'C:\Data\TestDataFile.ndf' + } + + It 'Should create a DataFile and return it with PassThru' { + $initialFileCount = $script:mockFileGroup.Files.Count + + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'TestDataFile2' -FileName 'C:\Data\TestDataFile2.ndf' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.DataFile' + $result.Name | Should -Be 'TestDataFile2' + $result.FileName | Should -Be 'C:\Data\TestDataFile2.ndf' + $result.Parent | Should -Be $script:mockFileGroup + $script:mockFileGroup.Files.Count | Should -Be ($initialFileCount + 1) + } + + It 'Should support Force parameter to bypass confirmation' { + $initialFileCount = $script:mockFileGroup.Files.Count + + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'ForcedDataFile' -FileName 'C:\Data\ForcedDataFile.ndf' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.DataFile' + $result.Name | Should -Be 'ForcedDataFile' + $result.Parent | Should -Be $script:mockFileGroup + $script:mockFileGroup.Files.Count | Should -Be ($initialFileCount + 1) + } + + It 'Should not add file when WhatIf is specified' { + $initialFileCount = $script:mockFileGroup.Files.Count + + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'DeclinedDataFile' -FileName 'C:\Data\DeclinedDataFile.ndf' -Force -ErrorAction 'Stop' -WhatIf + + $result | Should -BeNullOrEmpty + $script:mockFileGroup.Files.Count | Should -Be $initialFileCount + } + } + + Context 'When verifying DataFile properties' { + BeforeEach { + # Create a real SMO Database object + $script:mockDatabase = [Microsoft.SqlServer.Management.Smo.Database]::new() + $script:mockDatabase.Name = 'TestDatabase' + $script:mockDatabase.Parent = $script:serverObject + + $script:mockFileGroup = New-SqlDscFileGroup -Database $script:mockDatabase -Name 'TestFileGroup' -Force -ErrorAction 'Stop' + } + + It 'Should allow setting Size property on returned DataFile' { + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'TestDataFile' -FileName 'C:\Data\TestDataFile.ndf' -PassThru -Force -ErrorAction 'Stop' + $result.Size = 1024.0 + + $result.Size | Should -Be 1024.0 + } + + It 'Should allow setting Growth property on returned DataFile' { + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'TestDataFile' -FileName 'C:\Data\TestDataFile.ndf' -PassThru -Force -ErrorAction 'Stop' + $result.Growth = 64.0 + + $result.Growth | Should -Be 64.0 + } + + It 'Should allow setting GrowthType property on returned DataFile' { + $result = New-SqlDscDataFile -FileGroup $script:mockFileGroup -Name 'TestDataFile' -FileName 'C:\Data\TestDataFile.ndf' -PassThru -Force -ErrorAction 'Stop' + $result.GrowthType = [Microsoft.SqlServer.Management.Smo.FileGrowthType]::Percent + + $result.GrowthType | Should -Be 'Percent' + } + } +} diff --git a/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 b/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 index 7c79fb42dc..91f1791aca 100644 --- a/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 +++ b/tests/Integration/Commands/New-SqlDscDatabase.Integration.Tests.ps1 @@ -150,4 +150,64 @@ Describe 'New-SqlDscDatabase' -Tag @('Integration_SQL2017', 'Integration_SQL2019 Should -Throw } } + + Context 'When creating a database with file groups' { + BeforeAll { + $script:testDatabaseWithFileGroups = 'SqlDscTestDbFileGroups_' + (Get-Random) + + # Get the default data directory from the server + $script:dataDirectory = $script:serverObject.Settings.DefaultFile + + if (-not $script:dataDirectory) + { + $script:dataDirectory = $script:serverObject.Information.MasterDBPath + } + + # Ensure the directory exists + if (-not (Test-Path -Path $script:dataDirectory)) + { + $null = New-Item -Path $script:dataDirectory -ItemType Directory -Force + } + } + + AfterAll { + # Clean up test database + $dbToRemove = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseWithFileGroups -ErrorAction 'SilentlyContinue' + if ($dbToRemove) + { + $null = Remove-SqlDscDatabase -DatabaseObject $dbToRemove -Force -ErrorAction 'Stop' + } + } + + It 'Should create a database with custom file groups and data files' { + # Create PRIMARY filegroup specification with data file using -AsSpec parameters + $primaryFile = New-SqlDscDataFile -Name ($script:testDatabaseWithFileGroups + '_Primary') -FileName (Join-Path -Path $script:dataDirectory -ChildPath ($script:testDatabaseWithFileGroups + '_Primary.mdf')) -IsPrimaryFile -AsSpec + $primaryFileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile) -AsSpec + + # Create a secondary filegroup specification with data file using -AsSpec parameters + $secondaryFile = New-SqlDscDataFile -Name ($script:testDatabaseWithFileGroups + '_Secondary') -FileName (Join-Path -Path $script:dataDirectory -ChildPath ($script:testDatabaseWithFileGroups + '_Secondary.ndf')) -AsSpec + $secondaryFileGroup = New-SqlDscFileGroup -Name 'SecondaryFG' -Files @($secondaryFile) -AsSpec + + # Create database with file group specifications + $result = New-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseWithFileGroups -FileGroup @($primaryFileGroup, $secondaryFileGroup) -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:testDatabaseWithFileGroups + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + + # Verify the database exists with correct file groups + $createdDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testDatabaseWithFileGroups -Refresh -ErrorAction 'Stop' + $createdDb | Should -Not -BeNullOrEmpty + + # Verify PRIMARY filegroup exists + $createdDb.FileGroups['PRIMARY'] | Should -Not -BeNullOrEmpty + $createdDb.FileGroups['PRIMARY'].Files.Count | Should -Be 1 + $createdDb.FileGroups['PRIMARY'].Files[0].Name | Should -Be ($script:testDatabaseWithFileGroups + '_Primary') + + # Verify secondary filegroup exists + $createdDb.FileGroups['SecondaryFG'] | Should -Not -BeNullOrEmpty + $createdDb.FileGroups['SecondaryFG'].Files.Count | Should -Be 1 + $createdDb.FileGroups['SecondaryFG'].Files[0].Name | Should -Be ($script:testDatabaseWithFileGroups + '_Secondary') + } + } } diff --git a/tests/Integration/Commands/New-SqlDscDatabaseSnapshot.Integration.Tests.ps1 b/tests/Integration/Commands/New-SqlDscDatabaseSnapshot.Integration.Tests.ps1 new file mode 100644 index 0000000000..92638a9f35 --- /dev/null +++ b/tests/Integration/Commands/New-SqlDscDatabaseSnapshot.Integration.Tests.ps1 @@ -0,0 +1,230 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' +} + +Describe 'New-SqlDscDatabaseSnapshot' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $script:mockInstanceName = 'DSCSQLTEST' + + $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' + + # Source database names - using the persistent database created by New-SqlDscDatabase integration tests + $script:persistentSourceDatabase = 'SqlDscIntegrationTestDatabase_Persistent' + + # Snapshot names + $script:testSnapshotName = 'SqlDscTestSnapshot_' + (Get-Random) + $script:testSnapshotNameWithFileGroup = 'SqlDscTestSnapshotFG_' + (Get-Random) + $script:testSnapshotNameFromDbObject = 'SqlDscTestSnapshotDbObj_' + (Get-Random) + + # Verify the persistent database exists before proceeding + $sourceDb = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:persistentSourceDatabase -ErrorAction 'SilentlyContinue' + + if (-not $sourceDb) + { + throw "The source database '$script:persistentSourceDatabase' does not exist. Please ensure New-SqlDscDatabase integration tests have run successfully." + } + } + + AfterAll { + # Clean up test snapshots + $testSnapshotsToRemove = @( + $script:testSnapshotName, + $script:testSnapshotNameWithFileGroup, + $script:testSnapshotNameFromDbObject + ) + + foreach ($snapshotName in $testSnapshotsToRemove) + { + $existingSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $snapshotName -ErrorAction 'SilentlyContinue' + + if ($existingSnapshot) + { + $null = Remove-SqlDscDatabase -DatabaseObject $existingSnapshot -Force -ErrorAction 'Stop' + } + } + + Disconnect-SqlDscDatabaseEngine -ServerObject $script:serverObject + } + + Context 'When creating a database snapshot using ServerObject parameter set' { + AfterEach { + # Clean up snapshot created in this context to avoid file conflicts + $existingSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testSnapshotName -ErrorAction 'SilentlyContinue' + + if ($existingSnapshot) + { + $null = Remove-SqlDscDatabase -DatabaseObject $existingSnapshot -Force -ErrorAction 'Stop' + } + } + + It 'Should create a database snapshot successfully with minimal parameters' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $script:serverObject -Name $script:testSnapshotName -DatabaseName $script:persistentSourceDatabase -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:testSnapshotName + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + + # Verify the snapshot exists + $createdSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testSnapshotName -Refresh -ErrorAction 'Stop' + $createdSnapshot | Should -Not -BeNullOrEmpty + $createdSnapshot.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + } + + It 'Should throw error when trying to create a snapshot that already exists' { + # First create the snapshot + $null = New-SqlDscDatabaseSnapshot -ServerObject $script:serverObject -Name $script:testSnapshotName -DatabaseName $script:persistentSourceDatabase -Force -ErrorAction 'Stop' + + # Then try to create it again - should throw + { New-SqlDscDatabaseSnapshot -ServerObject $script:serverObject -Name $script:testSnapshotName -DatabaseName $script:persistentSourceDatabase -Force -ErrorAction 'Stop' } | + Should -Throw + } + } + + Context 'When creating a database snapshot using DatabaseObject parameter set' { + BeforeAll { + # Get the source database object + $script:sourceDatabaseObject = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:persistentSourceDatabase -ErrorAction 'Stop' + } + + AfterEach { + # Clean up snapshot created in this context to avoid file conflicts + $existingSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testSnapshotNameFromDbObject -ErrorAction 'SilentlyContinue' + + if ($existingSnapshot) + { + $null = Remove-SqlDscDatabase -DatabaseObject $existingSnapshot -Force -ErrorAction 'Stop' + } + } + + It 'Should create a database snapshot from DatabaseObject successfully' { + $result = New-SqlDscDatabaseSnapshot -DatabaseObject $script:sourceDatabaseObject -Name $script:testSnapshotNameFromDbObject -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:testSnapshotNameFromDbObject + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + + # Verify the snapshot exists + $createdSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testSnapshotNameFromDbObject -Refresh -ErrorAction 'Stop' + $createdSnapshot | Should -Not -BeNullOrEmpty + $createdSnapshot.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + } + } + + Context 'When creating a database snapshot with custom file groups' { + BeforeAll { + # Get the default data directory from the server + $script:dataDirectory = $script:serverObject.Settings.DefaultFile + + if (-not $script:dataDirectory) + { + $script:dataDirectory = $script:serverObject.Information.MasterDBPath + } + + # Ensure the directory exists + if (-not (Test-Path -Path $script:dataDirectory)) + { + $null = New-Item -Path $script:dataDirectory -ItemType Directory -Force + } + + # Get the source database for file group creation + $script:sourceDatabaseForFG = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:persistentSourceDatabase -ErrorAction 'Stop' + } + + AfterEach { + # Clean up snapshot created in this context to avoid file conflicts + $existingSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testSnapshotNameWithFileGroup -ErrorAction 'SilentlyContinue' + + if ($existingSnapshot) + { + $null = Remove-SqlDscDatabase -DatabaseObject $existingSnapshot -Force -ErrorAction 'Stop' + } + } + + It 'Should create a database snapshot with custom sparse file location' { + # Get the logical name of the source database's primary file + $sourceDatabase = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:persistentSourceDatabase -ErrorAction 'Stop' + $sourceLogicalFileName = $sourceDatabase.FileGroups['PRIMARY'].Files[0].Name + + # Create PRIMARY filegroup with sparse file using -AsSpec + # IMPORTANT: Must use the same logical name as the source database file + $sparseFilePath = Join-Path -Path $script:dataDirectory -ChildPath ($script:testSnapshotNameWithFileGroup + '_Sparse.ss') + $dataFileSpec = New-SqlDscDataFile -Name $sourceLogicalFileName -FileName $sparseFilePath -AsSpec + $primaryFileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($dataFileSpec) -AsSpec + + # Create snapshot with custom file group + $result = New-SqlDscDatabaseSnapshot -ServerObject $script:serverObject -Name $script:testSnapshotNameWithFileGroup -DatabaseName $script:persistentSourceDatabase -FileGroup @($primaryFileGroupSpec) -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:testSnapshotNameWithFileGroup + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.Database' + $result.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + + # Verify the snapshot exists with correct file configuration + $createdSnapshot = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:testSnapshotNameWithFileGroup -Refresh -ErrorAction 'Stop' + $createdSnapshot | Should -Not -BeNullOrEmpty + $createdSnapshot.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + + # Verify the sparse file was created with the correct path + $createdSnapshot.FileGroups['PRIMARY'] | Should -Not -BeNullOrEmpty + $createdSnapshot.FileGroups['PRIMARY'].Files.Count | Should -BeGreaterThan 0 + $createdSnapshot.FileGroups['PRIMARY'].Files[0].FileName | Should -Be $sparseFilePath + } + } + + Context 'When using the Refresh parameter' { + BeforeAll { + $script:refreshTestSnapshotName = 'SqlDscTestSnapshotRefresh_' + (Get-Random) + } + + AfterAll { + # Clean up the refresh test snapshot + $snapshotToRemove = Get-SqlDscDatabase -ServerObject $script:serverObject -Name $script:refreshTestSnapshotName -ErrorAction 'SilentlyContinue' + if ($snapshotToRemove) + { + $null = Remove-SqlDscDatabase -DatabaseObject $snapshotToRemove -Force -ErrorAction 'Stop' + } + } + + It 'Should refresh the database collection before creating snapshot' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $script:serverObject -Name $script:refreshTestSnapshotName -DatabaseName $script:persistentSourceDatabase -Refresh -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $script:refreshTestSnapshotName + $result.DatabaseSnapshotBaseName | Should -Be $script:persistentSourceDatabase + } + } +} diff --git a/tests/Integration/Commands/New-SqlDscFileGroup.Integration.Tests.ps1 b/tests/Integration/Commands/New-SqlDscFileGroup.Integration.Tests.ps1 new file mode 100644 index 0000000000..7b199704bb --- /dev/null +++ b/tests/Integration/Commands/New-SqlDscFileGroup.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 noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + # Import the SMO module to ensure real SMO types are available + Import-SqlDscPreferredModule +} + +Describe 'New-SqlDscFileGroup' -Tag @('Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') { + BeforeAll { + $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 + } + + Context 'When creating a standalone FileGroup with real SMO types' { + It 'Should create a standalone FileGroup successfully' { + $result = New-SqlDscFileGroup -Name 'TestFileGroup' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'TestFileGroup' + $result.Parent | Should -BeNullOrEmpty + } + + It 'Should create a standalone PRIMARY FileGroup successfully' { + $result = New-SqlDscFileGroup -Name 'PRIMARY' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'PRIMARY' + $result.Parent | Should -BeNullOrEmpty + } + } + + Context 'When creating a FileGroup with a Database object' { + BeforeAll { + # Create a real SMO Database object + $script:mockDatabase = [Microsoft.SqlServer.Management.Smo.Database]::new() + $script:mockDatabase.Name = 'TestDatabase' + $script:mockDatabase.Parent = $script:serverObject + } + + It 'Should create a FileGroup with Database successfully' { + $result = New-SqlDscFileGroup -Database $script:mockDatabase -Name 'TestFileGroup' -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'TestFileGroup' + $result.Parent | Should -Be $script:mockDatabase + } + + It 'Should support Force parameter to bypass confirmation' { + $result = New-SqlDscFileGroup -Database $script:mockDatabase -Name 'ForcedFileGroup' -Force + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'ForcedFileGroup' + $result.Parent | Should -Be $script:mockDatabase + } + + It 'Should return null when user declines confirmation' { + $result = New-SqlDscFileGroup -Database $script:mockDatabase -Name 'DeclinedFileGroup' -Confirm:$false -WhatIf + + $result | Should -BeNullOrEmpty + } + } +} diff --git a/tests/Integration/Commands/README.md b/tests/Integration/Commands/README.md index 27f0896d39..224a6bfbde 100644 --- a/tests/Integration/Commands/README.md +++ b/tests/Integration/Commands/README.md @@ -91,6 +91,7 @@ Revoke-SqlDscServerPermission | 4 | 4 (New-SqlDscLogin), 1 (Install-SqlDscServer Get-SqlDscDatabase | 4 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | - ConvertFrom-SqlDscDatabasePermission | 4 | 0 (Prerequisites) | - | - New-SqlDscDatabase | 4 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | SqlDscIntegrationTestDatabase_Persistent database +New-SqlDscDatabaseSnapshot | 5 | 4 (New-SqlDscDatabase), 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | - Get-SqlDscCompatibilityLevel | 4 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | - Set-SqlDscDatabaseProperty | 4 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | - Set-SqlDscDatabaseOwner | 4 | 1 (Install-SqlDscServer), 0 (Prerequisites) | DSCSQLTEST | - diff --git a/tests/QA/module.tests.ps1 b/tests/QA/module.tests.ps1 index 463822a9cb..8b975b1930 100644 --- a/tests/QA/module.tests.ps1 +++ b/tests/QA/module.tests.ps1 @@ -179,6 +179,66 @@ Describe 'Quality for module' -Tags 'TestQuality' { } } +Describe 'Comment-based help structure' -Tags 'helpQuality' { + Context 'Validating comment-based help structure for ' -ForEach $testCasesAllModuleFunction -Tag 'helpQuality' { + BeforeAll { + $functionFile = Get-ChildItem -Path $sourcePath -Recurse -Include "$Name.ps1" + + $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName + } + + It 'Should not have invalid help directives in comment-based help for ' { + # Valid help directives (case-insensitive) from: + # https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_comment_based_help + $validDirectives = @( + 'SYNOPSIS' + 'DESCRIPTION' + 'PARAMETER' + 'EXAMPLE' + 'INPUTS' + 'OUTPUTS' + 'NOTES' + 'LINK' + 'COMPONENT' + 'ROLE' + 'FUNCTIONALITY' + 'FORWARDHELPTARGETNAME' + 'FORWARDHELPCATEGORY' + 'REMOTEHELPRUNSPACE' + 'EXTERNALHELP' + ) + + # Find the comment-based help block + if ($scriptFileRawContent -match '(?s)<#(.*?)#>') + { + $helpBlock = $Matches[1] + + # Split into lines to check each one + $helpLines = $helpBlock -split "`n" + + $invalidDirectives = @() + + foreach ($line in $helpLines) + { + # Check if line starts with whitespace followed by a period and text + if ($line -match '^\s+\.([a-zA-Z]+)') + { + $directive = $Matches[1] + + # Check if it's a valid directive + if ($directive -notin $validDirectives) + { + $invalidDirectives += $directive + } + } + } + + $invalidDirectives | Should -BeNullOrEmpty -Because ('invalid help directives found that will break help parsing: {0}' -f ($invalidDirectives -join ', ')) + } + } + } +} + Describe 'Help for module' -Tags 'helpQuality' { Context 'Validating help for ' -ForEach $testCasesAllModuleFunction -Tag 'helpQuality' { BeforeAll { @@ -186,7 +246,16 @@ Describe 'Help for module' -Tags 'helpQuality' { $scriptFileRawContent = Get-Content -Raw -Path $functionFile.FullName - $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $null) + $parseErrors = $null + $abstractSyntaxTree = [System.Management.Automation.Language.Parser]::ParseInput($scriptFileRawContent, [ref] $null, [ref] $parseErrors) + + if ($parseErrors) + { + foreach ($parseError in $parseErrors) + { + Write-Warning -Message ('Parse error in {0}: {1}' -f $functionFile.FullName, $parseError.Message) + } + } $astSearchDelegate = { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] } diff --git a/tests/Unit/Classes/DatabaseFileGroupSpec.Tests.ps1 b/tests/Unit/Classes/DatabaseFileGroupSpec.Tests.ps1 new file mode 100644 index 0000000000..cd3250756d --- /dev/null +++ b/tests/Unit/Classes/DatabaseFileGroupSpec.Tests.ps1 @@ -0,0 +1,279 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + $env:SqlServerDscCI = $true + + $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') + + Remove-Item -Path 'env:SqlServerDscCI' + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'DatabaseFileGroupSpec' -Tag 'DatabaseFileGroupSpec' { + Context 'When instantiating the class' { + It 'Should create an instance with default constructor' { + $instance = InModuleScope -ScriptBlock { + [DatabaseFileGroupSpec]::new() + } + + $instance | Should -Not -BeNullOrEmpty + $instance.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + } + + It 'Should create an instance with Name only' { + $instance = InModuleScope -ScriptBlock { + [DatabaseFileGroupSpec]::new('PRIMARY') + } + + $instance | Should -Not -BeNullOrEmpty + $instance.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $instance.Name | Should -Be 'PRIMARY' + } + + It 'Should create an instance with Name and Files array' { + $instance = InModuleScope -ScriptBlock { + $files = @( + [DatabaseFileSpec]::new('File1', 'C:\Data\File1.mdf') + ) + [DatabaseFileGroupSpec]::new('PRIMARY', $files) + } + + $instance | Should -Not -BeNullOrEmpty + $instance.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $instance.Name | Should -Be 'PRIMARY' + $instance.Files | Should -HaveCount 1 + $instance.Files[0].Name | Should -Be 'File1' + $instance.Files[0].FileName | Should -Be 'C:\Data\File1.mdf' + } + } + + Context 'When setting properties using the parameterized constructor with Name only' { + It 'Should set Name property and initialize empty Files array' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]::new('MyFileGroup') + + $instance.Name | Should -Be 'MyFileGroup' + $instance.Files | Should -BeNullOrEmpty + } + } + } + + Context 'When setting properties using the parameterized constructor with Name and Files' { + It 'Should set Name and Files properties' { + InModuleScope -ScriptBlock { + $files = @( + [DatabaseFileSpec]::new('File1', 'C:\Data\File1.mdf'), + [DatabaseFileSpec]::new('File2', 'C:\Data\File2.ndf') + ) + $instance = [DatabaseFileGroupSpec]::new('SecondaryFG', $files) + + $instance.Name | Should -Be 'SecondaryFG' + $instance.Files | Should -HaveCount 2 + $instance.Files[0].Name | Should -Be 'File1' + $instance.Files[1].Name | Should -Be 'File2' + } + } + } + + Context 'When setting properties individually' { + It 'Should allow setting all properties' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]::new() + $instance.Name = 'TestFileGroup' + $instance.Files = @( + [DatabaseFileSpec]@{ Name = 'TestFile'; FileName = 'D:\Data\TestFile.ndf' } + ) + $instance.ReadOnly = $true + $instance.IsDefault = $true + + $instance.Name | Should -Be 'TestFileGroup' + $instance.Files | Should -HaveCount 1 + $instance.Files[0].Name | Should -Be 'TestFile' + $instance.ReadOnly | Should -BeTrue + $instance.IsDefault | Should -BeTrue + } + } + } + + Context 'When using default property values' { + It 'Should have null or false default values' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]::new() + + $instance.Name | Should -BeNullOrEmpty + $instance.Files | Should -BeNullOrEmpty + $instance.ReadOnly | Should -BeFalse + $instance.IsDefault | Should -BeFalse + } + } + } + + Context 'When using hashtable syntax for instantiation' { + It 'Should create instance with properties from hashtable' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]@{ + Name = 'DataFileGroup' + Files = @( + [DatabaseFileSpec]@{ + Name = 'DataFile1' + FileName = 'E:\Data\DataFile1.ndf' + Size = 100.0 + Growth = 10.0 + GrowthType = 'MB' + } + ) + ReadOnly = $false + IsDefault = $false + } + + $instance.Name | Should -Be 'DataFileGroup' + $instance.Files | Should -HaveCount 1 + $instance.Files[0].Name | Should -Be 'DataFile1' + $instance.Files[0].FileName | Should -Be 'E:\Data\DataFile1.ndf' + $instance.ReadOnly | Should -BeFalse + $instance.IsDefault | Should -BeFalse + } + } + } + + Context 'When creating PRIMARY file group' { + It 'Should set IsDefault to true for PRIMARY file group' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]@{ + Name = 'PRIMARY' + Files = @( + [DatabaseFileSpec]@{ + Name = 'MyDB' + FileName = 'C:\Data\MyDB.mdf' + IsPrimaryFile = $true + } + ) + IsDefault = $true + } + + $instance.Name | Should -Be 'PRIMARY' + $instance.Files[0].IsPrimaryFile | Should -BeTrue + $instance.IsDefault | Should -BeTrue + } + } + } + + Context 'When creating read-only file group' { + It 'Should set ReadOnly property' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]@{ + Name = 'ReadOnlyFG' + ReadOnly = $true + } + + $instance.Name | Should -Be 'ReadOnlyFG' + $instance.ReadOnly | Should -BeTrue + } + } + } + + Context 'When adding multiple files to a file group' { + It 'Should accept array of DatabaseFileSpec objects' { + InModuleScope -ScriptBlock { + $files = @( + [DatabaseFileSpec]@{ Name = 'File1'; FileName = 'C:\Data\File1.ndf'; Size = 50.0 } + [DatabaseFileSpec]@{ Name = 'File2'; FileName = 'C:\Data\File2.ndf'; Size = 100.0 } + [DatabaseFileSpec]@{ Name = 'File3'; FileName = 'D:\Data\File3.ndf'; Size = 150.0 } + ) + + $instance = [DatabaseFileGroupSpec]@{ + Name = 'MultiFileFG' + Files = $files + } + + $instance.Files | Should -HaveCount 3 + $instance.Files[0].Name | Should -Be 'File1' + $instance.Files[1].Name | Should -Be 'File2' + $instance.Files[2].Name | Should -Be 'File3' + $instance.Files[0].Size | Should -Be 50.0 + $instance.Files[1].Size | Should -Be 100.0 + $instance.Files[2].Size | Should -Be 150.0 + } + } + } + + Context 'When creating a file group without files' { + It 'Should allow empty Files array' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]@{ + Name = 'EmptyFG' + } + + $instance.Name | Should -Be 'EmptyFG' + $instance.Files | Should -BeNullOrEmpty + } + } + } + + Context 'When Files property contains DatabaseFileSpec with all properties set' { + It 'Should preserve all DatabaseFileSpec properties' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileGroupSpec]@{ + Name = 'DetailedFG' + Files = @( + [DatabaseFileSpec]@{ + Name = 'DetailedFile' + FileName = 'F:\Data\DetailedFile.ndf' + Size = 200.0 + MaxSize = 2000.0 + Growth = 20.0 + GrowthType = 'Percent' + IsPrimaryFile = $false + } + ) + } + + $file = $instance.Files[0] + $file.Name | Should -Be 'DetailedFile' + $file.FileName | Should -Be 'F:\Data\DetailedFile.ndf' + $file.Size | Should -Be 200.0 + $file.MaxSize | Should -Be 2000.0 + $file.Growth | Should -Be 20.0 + $file.GrowthType | Should -Be 'Percent' + $file.IsPrimaryFile | Should -BeFalse + } + } + } +} diff --git a/tests/Unit/Classes/DatabaseFileSpec.Tests.ps1 b/tests/Unit/Classes/DatabaseFileSpec.Tests.ps1 new file mode 100644 index 0000000000..861f2c34c6 --- /dev/null +++ b/tests/Unit/Classes/DatabaseFileSpec.Tests.ps1 @@ -0,0 +1,181 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + $env:SqlServerDscCI = $true + + $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') + + Remove-Item -Path 'env:SqlServerDscCI' + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'DatabaseFileSpec' -Tag 'DatabaseFileSpec' { + Context 'When instantiating the class' { + It 'Should not throw when instantiated with default constructor' { + InModuleScope -ScriptBlock { + [DatabaseFileSpec]::new() + } + } + + It 'Should not throw when instantiated with Name and FileName' { + InModuleScope -ScriptBlock { + [DatabaseFileSpec]::new('TestFile', 'C:\Data\TestFile.mdf') + } + } + } + + Context 'When setting properties using the parameterized constructor' { + It 'Should set Name and FileName properties' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]::new('MyFile', 'D:\SQLData\MyFile.mdf') + + $instance.Name | Should -Be 'MyFile' + $instance.FileName | Should -Be 'D:\SQLData\MyFile.mdf' + } + } + } + + Context 'When setting properties individually' { + It 'Should allow setting all properties' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]::new() + $instance.Name = 'DataFile1' + $instance.FileName = 'C:\Data\DataFile1.ndf' + $instance.Size = 100.0 + $instance.MaxSize = 1000.0 + $instance.Growth = 10.0 + $instance.GrowthType = 'MB' + $instance.IsPrimaryFile = $true + + $instance.Name | Should -Be 'DataFile1' + $instance.FileName | Should -Be 'C:\Data\DataFile1.ndf' + $instance.Size | Should -Be 100.0 + $instance.MaxSize | Should -Be 1000.0 + $instance.Growth | Should -Be 10.0 + $instance.GrowthType | Should -Be 'MB' + $instance.IsPrimaryFile | Should -BeTrue + } + } + } + + Context 'When using default property values' { + It 'Should have null or false default values' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]::new() + + $instance.Name | Should -BeNullOrEmpty + $instance.FileName | Should -BeNullOrEmpty + $instance.Size | Should -BeNullOrEmpty + $instance.MaxSize | Should -BeNullOrEmpty + $instance.Growth | Should -BeNullOrEmpty + $instance.GrowthType | Should -BeNullOrEmpty + $instance.IsPrimaryFile | Should -BeFalse + } + } + } + + Context 'When using hashtable syntax for instantiation' { + It 'Should create instance with properties from hashtable' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]@{ + Name = 'SecondaryFile' + FileName = 'E:\Data\SecondaryFile.ndf' + Size = 50.0 + MaxSize = 500.0 + Growth = 5.0 + GrowthType = 'Percent' + } + + $instance.Name | Should -Be 'SecondaryFile' + $instance.FileName | Should -Be 'E:\Data\SecondaryFile.ndf' + $instance.Size | Should -Be 50.0 + $instance.MaxSize | Should -Be 500.0 + $instance.Growth | Should -Be 5.0 + $instance.GrowthType | Should -Be 'Percent' + $instance.IsPrimaryFile | Should -BeFalse + } + } + } + + Context 'When specifying a primary file' { + It 'Should set IsPrimaryFile to true' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]@{ + Name = 'PrimaryFile' + FileName = 'C:\Data\MyDB.mdf' + IsPrimaryFile = $true + } + + $instance.Name | Should -Be 'PrimaryFile' + $instance.FileName | Should -Be 'C:\Data\MyDB.mdf' + $instance.IsPrimaryFile | Should -BeTrue + } + } + } + + Context 'When using different growth types' { + It 'Should accept MB as growth type' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]::new() + $instance.GrowthType = 'MB' + + $instance.GrowthType | Should -Be 'MB' + } + } + + It 'Should accept Percent as growth type' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]::new() + $instance.GrowthType = 'Percent' + + $instance.GrowthType | Should -Be 'Percent' + } + } + + It 'Should accept KB as growth type' { + InModuleScope -ScriptBlock { + $instance = [DatabaseFileSpec]::new() + $instance.GrowthType = 'KB' + + $instance.GrowthType | Should -Be 'KB' + } + } + } +} diff --git a/tests/Unit/Public/Add-SqlDscFileGroup.Tests.ps1 b/tests/Unit/Public/Add-SqlDscFileGroup.Tests.ps1 new file mode 100644 index 0000000000..5d4a6ce174 --- /dev/null +++ b/tests/Unit/Public/Add-SqlDscFileGroup.Tests.ps1 @@ -0,0 +1,149 @@ +<# + .SYNOPSIS + Unit tests for Add-SqlDscFileGroup. + + .DESCRIPTION + Unit tests for Add-SqlDscFileGroup. +#> + +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:dscModuleName + + # Loading SMO stub 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 'Add-SqlDscFileGroup' -Tag 'Public' { + Context 'When adding FileGroups to a Database' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'MyDatabase') + $mockFileGroupObject1 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject, 'FG1') + $mockFileGroupObject2 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject, 'FG2') + } + + It 'Should add a single FileGroup to the Database without returning output' { + { Add-SqlDscFileGroup -Database $mockDatabaseObject -FileGroup $mockFileGroupObject1 } | Should -Not -Throw + + $mockDatabaseObject.FileGroups.Count | Should -Be 1 + $mockDatabaseObject.FileGroups[0].Name | Should -Be 'FG1' + } + + It 'Should add a FileGroup and return it when PassThru is specified' { + $mockDatabaseObject2 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'MyDatabase2') + $mockFileGroupObject3 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject2, 'FG3') + + $result = Add-SqlDscFileGroup -Database $mockDatabaseObject2 -FileGroup $mockFileGroupObject3 -PassThru + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'FG3' + $mockDatabaseObject2.FileGroups.Count | Should -Be 1 + } + + It 'Should add multiple FileGroups from an array' { + $mockDatabaseObject3 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'MyDatabase3') + $mockFileGroupObject4 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject3, 'FG4') + $mockFileGroupObject5 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject3, 'FG5') + $fileGroupArray = @($mockFileGroupObject4, $mockFileGroupObject5) + + { Add-SqlDscFileGroup -Database $mockDatabaseObject3 -FileGroup $fileGroupArray } | Should -Not -Throw + + $mockDatabaseObject3.FileGroups.Count | Should -Be 2 + $mockDatabaseObject3.FileGroups[0].Name | Should -Be 'FG4' + $mockDatabaseObject3.FileGroups[1].Name | Should -Be 'FG5' + } + + It 'Should accept FileGroups from pipeline' { + $mockDatabaseObject4 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'MyDatabase4') + $mockFileGroupObject6 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject4, 'FG6') + $mockFileGroupObject7 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject4, 'FG7') + + { @($mockFileGroupObject6, $mockFileGroupObject7) | Add-SqlDscFileGroup -Database $mockDatabaseObject4 } | Should -Not -Throw + + $mockDatabaseObject4.FileGroups.Count | Should -Be 2 + $mockDatabaseObject4.FileGroups[0].Name | Should -Be 'FG6' + $mockDatabaseObject4.FileGroups[1].Name | Should -Be 'FG7' + } + + It 'Should accept FileGroups from pipeline with PassThru' { + $mockDatabaseObject5 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' -ArgumentList @($mockServerObject, 'MyDatabase5') + $mockFileGroupObject8 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject5, 'FG8') + $mockFileGroupObject9 = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList @($mockDatabaseObject5, 'FG9') + + $result = @($mockFileGroupObject8, $mockFileGroupObject9) | Add-SqlDscFileGroup -Database $mockDatabaseObject5 -PassThru + + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + $result[0].Name | Should -Be 'FG8' + $result[1].Name | Should -Be 'FG9' + $mockDatabaseObject5.FileGroups.Count | Should -Be 2 + } + } + + Context 'Parameter validation' { + It 'Should have Database as a mandatory parameter' { + (Get-Command -Name 'Add-SqlDscFileGroup').Parameters['Database'].Attributes.Mandatory | Should -BeTrue + } + + It 'Should have FileGroup as a mandatory parameter' { + (Get-Command -Name 'Add-SqlDscFileGroup').Parameters['FileGroup'].Attributes.Mandatory | Should -BeTrue + } + + It 'Should have PassThru as an optional parameter' { + (Get-Command -Name 'Add-SqlDscFileGroup').Parameters['PassThru'].Attributes.Mandatory | Should -BeFalse + } + + It 'Should have FileGroup parameter accept pipeline input' { + (Get-Command -Name 'Add-SqlDscFileGroup').Parameters['FileGroup'].Attributes.ValueFromPipeline | Should -BeTrue + } + + It 'Should have FileGroup parameter accept array input' { + (Get-Command -Name 'Add-SqlDscFileGroup').Parameters['FileGroup'].ParameterType.Name | Should -Be 'FileGroup[]' + } + } +} diff --git a/tests/Unit/Public/ConvertTo-SqlDscDataFile.Tests.ps1 b/tests/Unit/Public/ConvertTo-SqlDscDataFile.Tests.ps1 new file mode 100644 index 0000000000..57113832d8 --- /dev/null +++ b/tests/Unit/Public/ConvertTo-SqlDscDataFile.Tests.ps1 @@ -0,0 +1,147 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + # Loading mocked classes BEFORE importing the module (required for classes that reference SMO types) + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + $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 'ConvertTo-SqlDscDataFile' -Tag 'Public' { + Context 'When converting a DatabaseFileSpec to SMO DataFile' { + BeforeAll { + # Create mock FileGroup + $mockFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'PRIMARY' -Force + } + + It 'Should convert a basic file spec with only required properties' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.DataFile' + $result.Name | Should -Be 'TestFile' + $result.FileName | Should -Be 'C:\SQLData\TestFile.mdf' + } + + It 'Should convert a file spec with all optional properties set' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' ` + -Size 102400 -MaxSize 512000 -Growth 10240 -GrowthType 'KB' -IsPrimaryFile -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestFile' + $result.FileName | Should -Be 'C:\SQLData\TestFile.mdf' + $result.Size | Should -Be 102400 + $result.MaxSize | Should -Be 512000 + $result.Growth | Should -Be 10240 + $result.GrowthType | Should -Be 'KB' + $result.IsPrimaryFile | Should -Be $true + } + + It 'Should convert a file spec with Size property' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -Size 204800 -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result.Size | Should -Be 204800 + } + + It 'Should convert a file spec with MaxSize property' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -MaxSize 1024000 -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result.MaxSize | Should -Be 1024000 + } + + It 'Should convert a file spec with Growth property' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -Growth 20480 -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result.Growth | Should -Be 20480 + } + + It 'Should convert a file spec with GrowthType property set to Percent' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -GrowthType 'Percent' -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result.GrowthType | Should -Be 'Percent' + } + + It 'Should convert a file spec with IsPrimaryFile property' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -IsPrimaryFile -AsSpec + + $result = ConvertTo-SqlDscDataFile -FileGroupObject $mockFileGroup -DataFileSpec $fileSpec + + $result.IsPrimaryFile | Should -Be $true + } + } + + Context 'Parameter validation' { + BeforeAll { + $commandInfo = Get-Command -Name 'ConvertTo-SqlDscDataFile' + } + + It 'Should have FileGroupObject as a mandatory parameter' { + $parameterInfo = $commandInfo.Parameters['FileGroupObject'] + $parameterInfo.Attributes.Mandatory | Should -Contain $true + } + + It 'Should have DataFileSpec as a mandatory parameter' { + $parameterInfo = $commandInfo.Parameters['DataFileSpec'] + $parameterInfo.Attributes.Mandatory | Should -Contain $true + } + + It 'Should have OutputType set to Microsoft.SqlServer.Management.Smo.DataFile' { + $commandInfo.OutputType.Name | Should -Contain 'Microsoft.SqlServer.Management.Smo.DataFile' + } + } +} diff --git a/tests/Unit/Public/ConvertTo-SqlDscFileGroup.Tests.ps1 b/tests/Unit/Public/ConvertTo-SqlDscFileGroup.Tests.ps1 new file mode 100644 index 0000000000..a850073794 --- /dev/null +++ b/tests/Unit/Public/ConvertTo-SqlDscFileGroup.Tests.ps1 @@ -0,0 +1,181 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + # Loading mocked classes BEFORE importing the module (required for classes that reference SMO types) + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + $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 'ConvertTo-SqlDscFileGroup' -Tag 'Public' { + Context 'When converting a DatabaseFileGroupSpec to SMO FileGroup' { + BeforeAll { + # Create mock Database + $mockDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestDatabase' -Force + } + + It 'Should convert a basic file group spec with only required properties' { + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'PRIMARY' + } + + It 'Should convert a file group spec with ReadOnly property' { + $fileGroupSpec = New-SqlDscFileGroup -Name 'READONLY_FG' -ReadOnly -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result.Name | Should -Be 'READONLY_FG' + $result.ReadOnly | Should -Be $true + } + + It 'Should convert a file group spec with IsDefault property' { + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -IsDefault -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result.Name | Should -Be 'PRIMARY' + $result.IsDefault | Should -Be $true + } + + It 'Should convert a file group spec with a single data file' { + $fileSpec = New-SqlDscDataFile -Name 'TestFile' -FileName 'C:\SQLData\TestFile.mdf' -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($fileSpec) -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result.Name | Should -Be 'PRIMARY' + $result.Files.Count | Should -Be 1 + $result.Files[0].Name | Should -Be 'TestFile' + $result.Files[0].FileName | Should -Be 'C:\SQLData\TestFile.mdf' + } + + It 'Should convert a file group spec with multiple data files' { + $fileSpec1 = New-SqlDscDataFile -Name 'TestFile1' -FileName 'C:\SQLData\TestFile1.ndf' -Size 102400 -AsSpec + $fileSpec2 = New-SqlDscDataFile -Name 'TestFile2' -FileName 'C:\SQLData\TestFile2.ndf' -Size 204800 -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'SECONDARY' -Files @($fileSpec1, $fileSpec2) -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result.Name | Should -Be 'SECONDARY' + $result.Files.Count | Should -Be 2 + $result.Files[0].Name | Should -Be 'TestFile1' + $result.Files[0].Size | Should -Be 102400 + $result.Files[1].Name | Should -Be 'TestFile2' + $result.Files[1].Size | Should -Be 204800 + } + + It 'Should convert a file group spec with all properties and multiple files' { + $primaryFile = New-SqlDscDataFile -Name 'PrimaryFile' -FileName 'C:\SQLData\Primary.mdf' ` + -Size 102400 -MaxSize 512000 -Growth 10240 -GrowthType 'KB' -IsPrimaryFile -AsSpec + + $secondaryFile = New-SqlDscDataFile -Name 'SecondaryFile' -FileName 'C:\SQLData\Secondary.ndf' ` + -Size 204800 -MaxSize 1024000 -Growth 20480 -GrowthType 'KB' -AsSpec + + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile, $secondaryFile) ` + -IsDefault -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result.Name | Should -Be 'PRIMARY' + $result.IsDefault | Should -Be $true + $result.Files.Count | Should -Be 2 + $result.Files[0].Name | Should -Be 'PrimaryFile' + $result.Files[0].IsPrimaryFile | Should -Be $true + $result.Files[1].Name | Should -Be 'SecondaryFile' + } + + It 'Should convert a file group spec without files (empty Files array)' { + $fileGroupSpec = New-SqlDscFileGroup -Name 'EMPTY_FG' -AsSpec + + $result = ConvertTo-SqlDscFileGroup -DatabaseObject $mockDatabase -FileGroupSpec $fileGroupSpec + + $result.Name | Should -Be 'EMPTY_FG' + $result.Files.Count | Should -Be 0 + } + } + + Context 'Parameter validation' { + BeforeAll { + $commandInfo = Get-Command -Name 'ConvertTo-SqlDscFileGroup' + } + + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-DatabaseObject] [-FileGroupSpec] []' + } + ) { + $result = $commandInfo.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 DatabaseObject as a mandatory parameter' { + $parameterInfo = $commandInfo.Parameters['DatabaseObject'] + $parameterInfo.Attributes.Mandatory | Should -Contain $true + } + + It 'Should have FileGroupSpec as a mandatory parameter' { + $parameterInfo = $commandInfo.Parameters['FileGroupSpec'] + $parameterInfo.Attributes.Mandatory | Should -Contain $true + } + + It 'Should have OutputType set to Microsoft.SqlServer.Management.Smo.FileGroup' { + $commandInfo.OutputType.Name | Should -Contain 'Microsoft.SqlServer.Management.Smo.FileGroup' + } + } +} diff --git a/tests/Unit/Public/New-SqlDscDataFile.Tests.ps1 b/tests/Unit/Public/New-SqlDscDataFile.Tests.ps1 new file mode 100644 index 0000000000..28606e4d2b --- /dev/null +++ b/tests/Unit/Public/New-SqlDscDataFile.Tests.ps1 @@ -0,0 +1,300 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -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:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +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:moduleName -All | Remove-Module -Force + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'New-SqlDscDataFile' -Tag 'Public' { + Context 'When creating a new DataFile' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject.Name = 'TestDatabase' + + $mockFileGroupObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList $mockDatabaseObject, 'PRIMARY' + } + + It 'Should create a DataFile and add it to the FileGroup' { + $initialFileCount = $mockFileGroupObject.Files.Count + + $null = New-SqlDscDataFile -FileGroup $mockFileGroupObject -Name 'MyDataFile' -FileName 'C:\Data\MyDataFile.mdf' -Confirm:$false + + $mockFileGroupObject.Files.Count | Should -Be ($initialFileCount + 1) + $addedFile = $mockFileGroupObject.Files | Where-Object -FilterScript { $_.Name -eq 'MyDataFile' } + $addedFile | Should -Not -BeNullOrEmpty + $addedFile.FileName | Should -Be 'C:\Data\MyDataFile.mdf' + } + + It 'Should return the created DataFile when PassThru is specified' { + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -Name 'PassThruDataFile' -FileName 'C:\Data\PassThruDataFile.mdf' -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.DataFile' + $result.Name | Should -Be 'PassThruDataFile' + $result.FileName | Should -Be 'C:\Data\PassThruDataFile.mdf' + $result.Parent | Should -Be $mockFileGroupObject + } + + It 'Should not return anything when PassThru is not specified' { + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -Name 'NoPassThruDataFile' -FileName 'C:\Data\NoPassThruDataFile.mdf' -Confirm:$false + + $result | Should -BeNullOrEmpty + } + + It 'Should create a sparse file for database snapshot' { + $initialFileCount = $mockFileGroupObject.Files.Count + + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -Name 'MySnapshot_Data' -FileName 'C:\Snapshots\MySnapshot_Data.ss' -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'MySnapshot_Data' + $result.FileName | Should -Be 'C:\Snapshots\MySnapshot_Data.ss' + $result.Parent | Should -Be $mockFileGroupObject + $mockFileGroupObject.Files.Count | Should -Be ($initialFileCount + 1) + } + + It 'Should support Force parameter to bypass confirmation' { + $initialFileCount = $mockFileGroupObject.Files.Count + + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -Name 'ForcedDataFile' -FileName 'C:\Data\ForcedDataFile.mdf' -PassThru -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'ForcedDataFile' + $result.FileName | Should -Be 'C:\Data\ForcedDataFile.mdf' + $result.Parent | Should -Be $mockFileGroupObject + $mockFileGroupObject.Files.Count | Should -Be ($initialFileCount + 1) + } + + It 'Should not add file when WhatIf is specified' { + $initialFileCount = $mockFileGroupObject.Files.Count + + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -Name 'WhatIfDataFile' -FileName 'C:\Data\WhatIfDataFile.mdf' -WhatIf + + $result | Should -BeNullOrEmpty + $mockFileGroupObject.Files.Count | Should -Be $initialFileCount + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set Standard' -ForEach @( + @{ + ExpectedParameterSetName = 'Standard' + ExpectedParameters = '-FileGroup -Name -FileName [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscDataFile').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 FromSpec' -ForEach @( + @{ + ExpectedParameterSetName = 'FromSpec' + ExpectedParameters = '-FileGroup -DataFileSpec [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscDataFile').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 AsSpec' -ForEach @( + @{ + ExpectedParameterSetName = 'AsSpec' + ExpectedParameters = '-Name -FileName -AsSpec [-Size ] [-MaxSize ] [-Growth ] [-GrowthType ] [-IsPrimaryFile] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscDataFile').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 FileGroup as a mandatory parameter in Standard parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['FileGroup'] + $standardSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'Standard' } + $standardSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have FileGroup as a mandatory parameter in FromSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['FileGroup'] + $fromSpecSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'FromSpec' } + $fromSpecSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter in Standard parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['Name'] + $standardSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'Standard' } + $standardSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['Name'] + $asSpecSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'AsSpec' } + $asSpecSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have FileName as a mandatory parameter in Standard parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['FileName'] + $standardSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'Standard' } + $standardSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have FileName as a mandatory parameter in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['FileName'] + $asSpecSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'AsSpec' } + $asSpecSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have DataFileSpec as a mandatory parameter in FromSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['DataFileSpec'] + $fromSpecSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'FromSpec' } + $fromSpecSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have ValidateNotNull attribute on DataFileSpec parameter' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['DataFileSpec'] + $validateNotNullAttribute = $parameterInfo.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateNotNullAttribute] } + $validateNotNullAttribute | Should -Not -BeNullOrEmpty + } + + It 'Should have AsSpec as a mandatory parameter in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDataFile').Parameters['AsSpec'] + $asSpecSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'AsSpec' } + $asSpecSetAttribute.Mandatory | Should -BeTrue + } + } + + Context 'When creating a DataFile with AsSpec parameter set' { + It 'Should return a DatabaseFileSpec object' { + $result = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -AsSpec + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'DatabaseFileSpec' + $result.Name | Should -Be 'MyDB_Primary' + $result.FileName | Should -Be 'D:\SQLData\MyDB.mdf' + } + + It 'Should set IsPrimaryFile when specified' { + $result = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -IsPrimaryFile -AsSpec + + $result | Should -Not -BeNullOrEmpty + $result.IsPrimaryFile | Should -BeTrue + } + + It 'Should set Size, MaxSize, Growth, and GrowthType when specified' { + $result = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -Size 102400 -MaxSize 5242880 -Growth 10240 -GrowthType 'KB' -AsSpec + + $result | Should -Not -BeNullOrEmpty + $result.Size | Should -Be 102400 + $result.MaxSize | Should -Be 5242880 + $result.Growth | Should -Be 10240 + $result.GrowthType | Should -Be 'KB' + } + } + + Context 'When creating a DataFile from a DatabaseFileSpec' { + BeforeAll { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject.Name = 'TestDatabase' + } + + It 'Should create a DataFile from a DatabaseFileSpec in the PRIMARY filegroup' { + $mockFileGroupObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList $mockDatabaseObject, 'PRIMARY' + + $fileSpec = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -IsPrimaryFile -AsSpec + + $initialFileCount = $mockFileGroupObject.Files.Count + + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -DataFileSpec $fileSpec -PassThru -Force + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.DataFile' + $mockFileGroupObject.Files.Count | Should -Be ($initialFileCount + 1) + } + + It 'Should throw an error when IsPrimaryFile is specified but filegroup is not PRIMARY' { + $mockFileGroupObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList $mockDatabaseObject, 'SECONDARY' + + $fileSpec = New-SqlDscDataFile -Name 'MyDB_Primary' -FileName 'D:\SQLData\MyDB.mdf' -IsPrimaryFile -AsSpec + + { New-SqlDscDataFile -FileGroup $mockFileGroupObject -DataFileSpec $fileSpec -Force -ErrorAction Stop } | + Should -Throw -ExpectedMessage '*The primary file must reside in the PRIMARY filegroup*' + } + + It 'Should create a DataFile from a DatabaseFileSpec without IsPrimaryFile in a non-PRIMARY filegroup' { + $mockFileGroupObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList $mockDatabaseObject, 'SECONDARY' + + $fileSpec = New-SqlDscDataFile -Name 'MyDB_Secondary' -FileName 'D:\SQLData\MyDB_Secondary.ndf' -AsSpec + + $initialFileCount = $mockFileGroupObject.Files.Count + + $result = New-SqlDscDataFile -FileGroup $mockFileGroupObject -DataFileSpec $fileSpec -PassThru -Force + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.DataFile' + $mockFileGroupObject.Files.Count | Should -Be ($initialFileCount + 1) + } + } +} diff --git a/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 b/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 index 77931c1125..26e291244c 100644 --- a/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 +++ b/tests/Unit/Public/New-SqlDscDatabase.Tests.ps1 @@ -157,7 +157,7 @@ Describe 'New-SqlDscDatabase' -Tag 'Public' { It 'Should have the correct parameters in parameter set Database' -ForEach @( @{ ExpectedParameterSetName = 'Database' - ExpectedParameters = '-ServerObject -Name [-Collation ] [-CatalogCollation ] [-CompatibilityLevel ] [-RecoveryModel ] [-OwnerName ] [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + ExpectedParameters = '-ServerObject -Name [-Collation ] [-CatalogCollation ] [-CompatibilityLevel ] [-RecoveryModel ] [-OwnerName ] [-FileGroup ] [-Force] [-Refresh] [-WhatIf] [-Confirm] []' } ) { $result = (Get-Command -Name 'New-SqlDscDatabase').ParameterSets | @@ -174,7 +174,7 @@ Describe 'New-SqlDscDatabase' -Tag 'Public' { It 'Should have the correct parameters in parameter set Snapshot' -ForEach @( @{ ExpectedParameterSetName = 'Snapshot' - ExpectedParameters = '-ServerObject -Name -DatabaseSnapshotBaseName [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + ExpectedParameters = '-ServerObject -Name -DatabaseSnapshotBaseName [-FileGroup ] [-Force] [-Refresh] [-WhatIf] [-Confirm] []' } ) { $result = (Get-Command -Name 'New-SqlDscDatabase').ParameterSets | @@ -254,4 +254,158 @@ Describe 'New-SqlDscDatabase' -Tag 'Public' { Should -Throw -ExpectedMessage '*does not exist*' } } + + Context 'When creating a database snapshot with FileGroup' { + 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 + + # Mock source database + $mockSourceDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSourceDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'SourceDatabase' -Force + + $mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Databases' -Value { + return @{ + 'SourceDatabase' = $mockSourceDatabase + } | Add-Member -MemberType 'ScriptMethod' -Name 'Refresh' -Value { + # Mock implementation + } -PassThru -Force + } -Force + + Mock -CommandName 'New-Object' -ParameterFilter { $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Database' } -MockWith { + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject.Name = $ArgumentList[1] + $mockDatabaseObject.DatabaseSnapshotBaseName = $null + + $mockDatabaseObject | Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { + # Mock implementation + } -Force + return $mockDatabaseObject + } + + # Mock the helper commands used by New-SqlDscDatabase + Mock -CommandName 'New-SqlDscFileGroup' -ParameterFilter { $null -ne $FileGroupSpec } -MockWith { + $mockFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $FileGroupSpec.Name -Force + return $mockFileGroup + } + + Mock -CommandName 'Add-SqlDscFileGroup' + + # Mock helper commands used in tests + Mock -CommandName 'New-SqlDscDataFile' -MockWith { + return [DatabaseFileSpec]@{ + Name = $Name + FileName = $FileName + } + } + + Mock -CommandName 'New-SqlDscFileGroup' -ParameterFilter { $null -eq $FileGroupSpec } -MockWith { + return [DatabaseFileGroupSpec]@{ + Name = $Name + Files = $Files + } + } + } + + It 'Should create a database snapshot with FileGroup spec successfully' { + # Create file spec using New-SqlDscDataFile with -AsSpec + $fileSpec = New-SqlDscDataFile -Name 'TestSnapshot_Data' -FileName 'C:\Snapshots\TestSnapshot_Data.ss' -AsSpec + + # Create filegroup spec using New-SqlDscFileGroup with -AsSpec + $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($fileSpec) -AsSpec + + $result = New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestSnapshot' -DatabaseSnapshotBaseName 'SourceDatabase' -FileGroup @($fileGroupSpec) -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestSnapshot' + $result.DatabaseSnapshotBaseName | Should -Be 'SourceDatabase' + Should -Invoke -CommandName 'New-SqlDscFileGroup' -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'Add-SqlDscFileGroup' -Exactly -Times 1 -Scope It + } + } + + Context 'When creating a database with custom file groups using spec objects' { + 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 + + 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 'ScriptMethod' -Name 'Create' -Value { + # Mock implementation + } -Force + return $mockDatabaseObject + } + + Mock -CommandName 'New-SqlDscFileGroup' -ParameterFilter { $null -ne $FileGroupSpec } -MockWith { + $mockFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $FileGroupSpec.Name -Force + return $mockFileGroup + } + + Mock -CommandName 'Add-SqlDscFileGroup' + + # Mock helper commands used in tests + Mock -CommandName 'New-SqlDscDataFile' -MockWith { + return [DatabaseFileSpec]@{ + Name = $Name + FileName = $FileName + Size = $Size + Growth = $Growth + GrowthType = $GrowthType + IsPrimaryFile = $IsPrimaryFile.IsPresent + } + } + + Mock -CommandName 'New-SqlDscFileGroup' -ParameterFilter { $null -eq $FileGroupSpec } -MockWith { + return [DatabaseFileGroupSpec]@{ + Name = $Name + Files = $Files + IsDefault = $IsDefault.IsPresent + } + } + } + + It 'Should create a database with custom PRIMARY filegroup using -AsSpec parameters' { + # Create file spec with parameters using -AsSpec + $primaryFile = New-SqlDscDataFile -Name 'TestDB_Primary' -FileName 'D:\SQLData\TestDB.mdf' -Size 102400 -Growth 10240 -GrowthType 'KB' -IsPrimaryFile -AsSpec + + # Create filegroup spec with parameters using -AsSpec + $primaryFileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile) -IsDefault -AsSpec + + $result = New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDB' -FileGroup @($primaryFileGroup) -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestDB' + Should -Invoke -CommandName 'New-SqlDscFileGroup' -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'Add-SqlDscFileGroup' -Exactly -Times 1 -Scope It + } + + It 'Should create a database with multiple filegroups using -AsSpec parameters' { + # Create PRIMARY filegroup + $primaryFile = New-SqlDscDataFile -Name 'TestDB_Primary' -FileName 'D:\SQLData\TestDB.mdf' -Size 102400 -IsPrimaryFile -AsSpec + $primaryFileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile) -IsDefault -AsSpec + + # Create secondary filegroup + $secondaryFile = New-SqlDscDataFile -Name 'TestDB_Secondary' -FileName 'E:\SQLData\TestDB.ndf' -Size 204800 -AsSpec + $secondaryFileGroup = New-SqlDscFileGroup -Name 'SECONDARY' -Files @($secondaryFile) -AsSpec + + $result = New-SqlDscDatabase -ServerObject $mockServerObject -Name 'TestDB' -FileGroup @($primaryFileGroup, $secondaryFileGroup) -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestDB' + Should -Invoke -CommandName 'New-SqlDscFileGroup' -Exactly -Times 2 -Scope It + Should -Invoke -CommandName 'Add-SqlDscFileGroup' -Exactly -Times 2 -Scope It + } + } } diff --git a/tests/Unit/Public/New-SqlDscDatabaseSnapshot.Tests.ps1 b/tests/Unit/Public/New-SqlDscDatabaseSnapshot.Tests.ps1 new file mode 100644 index 0000000000..dd2bd7a128 --- /dev/null +++ b/tests/Unit/Public/New-SqlDscDatabaseSnapshot.Tests.ps1 @@ -0,0 +1,471 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + + # Define platform-appropriate paths for use in mocks and assertions + if ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5) + { + $script:mockDefaultDataPath = 'C:\mssql\data' + $script:mockMasterDbPath = 'C:\mssql\master' + } + else + { + $script:mockDefaultDataPath = '/var/opt/mssql/data' + $script:mockMasterDbPath = '/var/opt/mssql/master' + } +} + +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-SqlDscDatabaseSnapshot' -Tag 'Public' { + Context 'When creating a database snapshot using ServerObject parameter set' { + 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 'EngineEdition' -Value 'Enterprise' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'Edition' -Value 'Enterprise Edition' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'VersionMajor' -Value 15 -Force + + # Mock Settings for default data directory + $mockSettings = New-Object -TypeName 'PSObject' + $mockSettings | Add-Member -MemberType 'NoteProperty' -Name 'DefaultFile' -Value $script:mockDefaultDataPath -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'Settings' -Value $mockSettings -Force + + # Mock source database with file groups and files + $mockSourceDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSourceDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'SourceDatabase' -Force + + # Mock file group and data file + $mockDataFile = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.DataFile' + $mockDataFile | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'SourceDatabase' -Force + $mockDataFile | Add-Member -MemberType 'NoteProperty' -Name 'FileName' -Value (Join-Path -Path $script:mockDefaultDataPath -ChildPath 'SourceDatabase.mdf') -Force + + $mockFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'PRIMARY' -Force + $mockFileGroup | Add-Member -MemberType 'ScriptProperty' -Name 'Files' -Value { + return @($mockDataFile) + } -Force + + $mockSourceDatabase | Add-Member -MemberType 'ScriptProperty' -Name 'FileGroups' -Value { + return @($mockFileGroup) + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockSourceDatabase + } + + Mock -CommandName 'New-SqlDscDatabase' -MockWith { + $mockSnapshotDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $Name -Force + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'DatabaseSnapshotBaseName' -Value $DatabaseSnapshotBaseName -Force + return $mockSnapshotDatabase + } + } + + It 'Should create a database snapshot successfully with minimal parameters' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObject -Name 'TestSnapshot' -DatabaseName 'SourceDatabase' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestSnapshot' + $result.DatabaseSnapshotBaseName | Should -Be 'SourceDatabase' + Should -Invoke -CommandName 'New-SqlDscDatabase' -Exactly -Times 1 + Should -Invoke -CommandName 'Get-SqlDscDatabase' -Exactly -Times 1 + } + + It 'Should auto-generate file groups when not specified' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObject -Name 'TestSnapshot' -DatabaseName 'SourceDatabase' -Force + + Should -Invoke -CommandName 'Get-SqlDscDatabase' -ParameterFilter { + $ServerObject.InstanceName -eq 'TestInstance' -and + $Name -eq 'SourceDatabase' -and + $ErrorAction -eq 'Stop' + } -Exactly -Times 1 + + Should -Invoke -CommandName 'New-SqlDscDatabase' -ParameterFilter { + $expectedPath = Join-Path -Path $script:mockDefaultDataPath -ChildPath 'SourceDatabase.ss' + $FileGroup -and + $FileGroup.Count -eq 1 -and + $FileGroup[0].Name -eq 'PRIMARY' -and + $FileGroup[0].Files.Count -eq 1 -and + $FileGroup[0].Files[0].Name -eq 'SourceDatabase' -and + $FileGroup[0].Files[0].FileName -eq $expectedPath + } -Exactly -Times 1 + } + + It 'Should pass the correct parameters to New-SqlDscDatabase' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObject -Name 'MySnapshot' -DatabaseName 'SourceDatabase' -Force + + Should -Invoke -CommandName 'New-SqlDscDatabase' -ParameterFilter { + $ServerObject.InstanceName -eq 'TestInstance' -and + $Name -eq 'MySnapshot' -and + $DatabaseSnapshotBaseName -eq 'SourceDatabase' -and + $Force -eq $true + } -Exactly -Times 1 + } + + It 'Should pass Refresh parameter to Get-SqlDscDatabase when specified' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObject -Name 'TestSnapshot' -DatabaseName 'SourceDatabase' -Refresh -Force + + Should -Invoke -CommandName 'Get-SqlDscDatabase' -ParameterFilter { + $Refresh -eq $true + } -Exactly -Times 1 + + Should -Invoke -CommandName 'New-SqlDscDatabase' -ParameterFilter { + $Refresh -eq $true + } -Exactly -Times 1 + } + + It 'Should pass FileGroup parameter when specified' { + # Create a mock DatabaseFileGroupSpec using InModuleScope to access internal classes + $mockFileGroupSpec = InModuleScope -ScriptBlock { + $mockDataFileSpec = [DatabaseFileSpec]@{ + Name = 'TestData' + FileName = 'C:\Snapshots\TestData.ss' + } + + [DatabaseFileGroupSpec]@{ + Name = 'PRIMARY' + Files = @($mockDataFileSpec) + } + } + + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObject -Name 'TestSnapshot' -DatabaseName 'SourceDatabase' -FileGroup @($mockFileGroupSpec) -Force + + # Should not call Get-SqlDscDatabase when FileGroup is specified + Should -Invoke -CommandName 'Get-SqlDscDatabase' -Exactly -Times 0 -Scope It + + Should -Invoke -CommandName 'New-SqlDscDatabase' -ParameterFilter { + $FileGroup -and $FileGroup.Count -eq 1 -and $FileGroup[0].Name -eq 'PRIMARY' + } -Exactly -Times 1 + } + + It 'Should use MasterDBPath when DefaultFile is not set' { + # Create a server object without DefaultFile set + $mockServerObjectNoDefaultFile = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectNoDefaultFile | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectNoDefaultFile | Add-Member -MemberType 'NoteProperty' -Name 'EngineEdition' -Value 'Enterprise' -Force + $mockServerObjectNoDefaultFile | Add-Member -MemberType 'NoteProperty' -Name 'Edition' -Value 'Enterprise Edition' -Force + + $mockSettingsNoDefault = New-Object -TypeName 'PSObject' + $mockSettingsNoDefault | Add-Member -MemberType 'NoteProperty' -Name 'DefaultFile' -Value $null -Force + $mockServerObjectNoDefaultFile | Add-Member -MemberType 'NoteProperty' -Name 'Settings' -Value $mockSettingsNoDefault -Force + + $mockInformation = New-Object -TypeName 'PSObject' + $mockInformation | Add-Member -MemberType 'NoteProperty' -Name 'MasterDBPath' -Value $script:mockMasterDbPath -Force + $mockServerObjectNoDefaultFile | Add-Member -MemberType 'NoteProperty' -Name 'Information' -Value $mockInformation -Force + + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObjectNoDefaultFile -Name 'TestSnapshot' -DatabaseName 'SourceDatabase' -Force + + Should -Invoke -CommandName 'New-SqlDscDatabase' -ParameterFilter { + $expectedPath = Join-Path -Path $script:mockMasterDbPath -ChildPath 'SourceDatabase.ss' + $FileGroup[0].Files[0].FileName -eq $expectedPath + } -Exactly -Times 1 + } + } + + Context 'When creating a database snapshot using DatabaseObject parameter set' { + 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 'EngineEdition' -Value 'Enterprise' -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'Edition' -Value 'Enterprise Edition' -Force + + # Mock Settings for default data directory + $mockSettings = New-Object -TypeName 'PSObject' + $mockSettings | Add-Member -MemberType 'NoteProperty' -Name 'DefaultFile' -Value $script:mockDefaultDataPath -Force + $mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'Settings' -Value $mockSettings -Force + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Parent' -Value $mockServerObject -Force + + # Mock file group and data file for DatabaseObject + $mockDataFile = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.DataFile' + $mockDataFile | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + $mockDataFile | Add-Member -MemberType 'NoteProperty' -Name 'FileName' -Value (Join-Path -Path $script:mockDefaultDataPath -ChildPath 'MyDatabase.mdf') -Force + + $mockFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'PRIMARY' -Force + $mockFileGroup | Add-Member -MemberType 'ScriptProperty' -Name 'Files' -Value { + return @($mockDataFile) + } -Force + + $mockDatabaseObject | Add-Member -MemberType 'ScriptProperty' -Name 'FileGroups' -Value { + return @($mockFileGroup) + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDatabaseObject + } + + Mock -CommandName 'New-SqlDscDatabase' -MockWith { + $mockSnapshotDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $Name -Force + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'DatabaseSnapshotBaseName' -Value $DatabaseSnapshotBaseName -Force + return $mockSnapshotDatabase + } + } + + It 'Should create a database snapshot from DatabaseObject successfully' { + $result = New-SqlDscDatabaseSnapshot -DatabaseObject $mockDatabaseObject -Name 'TestSnapshot' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestSnapshot' + $result.DatabaseSnapshotBaseName | Should -Be 'MyDatabase' + Should -Invoke -CommandName 'New-SqlDscDatabase' -Exactly -Times 1 + Should -Invoke -CommandName 'Get-SqlDscDatabase' -Exactly -Times 1 + } + + It 'Should pass the correct parameters to New-SqlDscDatabase from DatabaseObject' { + $result = New-SqlDscDatabaseSnapshot -DatabaseObject $mockDatabaseObject -Name 'MySnapshot' -Force + + Should -Invoke -CommandName 'New-SqlDscDatabase' -ParameterFilter { + $ServerObject.InstanceName -eq 'TestInstance' -and + $Name -eq 'MySnapshot' -and + $DatabaseSnapshotBaseName -eq 'MyDatabase' -and + $Force -eq $true + } -Exactly -Times 1 + } + } + + Context 'When SQL Server edition does not support snapshots' { + BeforeAll { + $mockServerObjectStandard = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectStandard | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectStandard | Add-Member -MemberType 'NoteProperty' -Name 'EngineEdition' -Value 'Standard' -Force + $mockServerObjectStandard | Add-Member -MemberType 'NoteProperty' -Name 'Edition' -Value 'Standard Edition' -Force + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + $mockDatabaseObject | Add-Member -MemberType 'NoteProperty' -Name 'Parent' -Value $mockServerObjectStandard -Force + + Mock -CommandName 'New-SqlDscDatabase' + } + + It 'Should throw error when SQL Server edition does not support snapshots' { + $errorRecord = { New-SqlDscDatabaseSnapshot -ServerObject $mockServerObjectStandard -Name 'TestSnapshot' -DatabaseName 'MyDatabase' -Force } | + Should -Throw -ExpectedMessage '*not supported*' -PassThru + + $errorRecord.Exception.Message | Should -BeLike '*not supported*' + $errorRecord.FullyQualifiedErrorId | Should -Be 'NSDS0001,New-SqlDscDatabaseSnapshot' + $errorRecord.CategoryInfo.Category | Should -Be 'InvalidOperation' + } + + It 'Should throw error when using DatabaseObject with unsupported edition' { + { New-SqlDscDatabaseSnapshot -DatabaseObject $mockDatabaseObject -Name 'TestSnapshot' -Force } | + Should -Throw -ExpectedMessage '*not supported*' + } + + It 'Should not call New-SqlDscDatabase when edition is not supported' { + { New-SqlDscDatabaseSnapshot -ServerObject $mockServerObjectStandard -Name 'TestSnapshot' -DatabaseName 'MyDatabase' -Force } | + Should -Throw + + Should -Invoke -CommandName 'New-SqlDscDatabase' -Exactly -Times 0 + } + } + + Context 'When SQL Server edition is Developer' { + BeforeAll { + $mockServerObjectDeveloper = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectDeveloper | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectDeveloper | Add-Member -MemberType 'NoteProperty' -Name 'EngineEdition' -Value 'Standard' -Force + $mockServerObjectDeveloper | Add-Member -MemberType 'NoteProperty' -Name 'Edition' -Value 'Developer Edition' -Force + + # Mock Settings for default data directory + $mockSettings = New-Object -TypeName 'PSObject' + $mockSettings | Add-Member -MemberType 'NoteProperty' -Name 'DefaultFile' -Value $script:mockDefaultDataPath -Force + $mockServerObjectDeveloper | Add-Member -MemberType 'NoteProperty' -Name 'Settings' -Value $mockSettings -Force + + # Mock source database + $mockDevSourceDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDevSourceDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + + $mockDevDataFile = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.DataFile' + $mockDevDataFile | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + $mockDevDataFile | Add-Member -MemberType 'NoteProperty' -Name 'FileName' -Value (Join-Path -Path $script:mockDefaultDataPath -ChildPath 'MyDatabase.mdf') -Force + + $mockDevFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockDevFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'PRIMARY' -Force + $mockDevFileGroup | Add-Member -MemberType 'ScriptProperty' -Name 'Files' -Value { + return @($mockDevDataFile) + } -Force + + $mockDevSourceDatabase | Add-Member -MemberType 'ScriptProperty' -Name 'FileGroups' -Value { + return @($mockDevFileGroup) + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockDevSourceDatabase + } + + Mock -CommandName 'New-SqlDscDatabase' -MockWith { + $mockSnapshotDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $Name -Force + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'DatabaseSnapshotBaseName' -Value $DatabaseSnapshotBaseName -Force + return $mockSnapshotDatabase + } + } + + It 'Should create a database snapshot on Developer edition' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObjectDeveloper -Name 'TestSnapshot' -DatabaseName 'MyDatabase' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestSnapshot' + Should -Invoke -CommandName 'New-SqlDscDatabase' -Exactly -Times 1 + } + } + + Context 'When SQL Server edition is Evaluation' { + BeforeAll { + $mockServerObjectEvaluation = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObjectEvaluation | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force + $mockServerObjectEvaluation | Add-Member -MemberType 'NoteProperty' -Name 'EngineEdition' -Value 'Standard' -Force + $mockServerObjectEvaluation | Add-Member -MemberType 'NoteProperty' -Name 'Edition' -Value 'Evaluation Edition' -Force + + # Mock Settings for default data directory + $mockSettings = New-Object -TypeName 'PSObject' + $mockSettings | Add-Member -MemberType 'NoteProperty' -Name 'DefaultFile' -Value $script:mockDefaultDataPath -Force + $mockServerObjectEvaluation | Add-Member -MemberType 'NoteProperty' -Name 'Settings' -Value $mockSettings -Force + + # Mock source database + $mockEvalSourceDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockEvalSourceDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + + $mockEvalDataFile = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.DataFile' + $mockEvalDataFile | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'MyDatabase' -Force + $mockEvalDataFile | Add-Member -MemberType 'NoteProperty' -Name 'FileName' -Value (Join-Path -Path $script:mockDefaultDataPath -ChildPath 'MyDatabase.mdf') -Force + + $mockEvalFileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' + $mockEvalFileGroup | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'PRIMARY' -Force + $mockEvalFileGroup | Add-Member -MemberType 'ScriptProperty' -Name 'Files' -Value { + return @($mockEvalDataFile) + } -Force + + $mockEvalSourceDatabase | Add-Member -MemberType 'ScriptProperty' -Name 'FileGroups' -Value { + return @($mockEvalFileGroup) + } -Force + + Mock -CommandName 'Get-SqlDscDatabase' -MockWith { + return $mockEvalSourceDatabase + } + + Mock -CommandName 'New-SqlDscDatabase' -MockWith { + $mockSnapshotDatabase = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $Name -Force + $mockSnapshotDatabase | Add-Member -MemberType 'NoteProperty' -Name 'DatabaseSnapshotBaseName' -Value $DatabaseSnapshotBaseName -Force + return $mockSnapshotDatabase + } + } + + It 'Should create a database snapshot on Evaluation edition' { + $result = New-SqlDscDatabaseSnapshot -ServerObject $mockServerObjectEvaluation -Name 'TestSnapshot' -DatabaseName 'MyDatabase' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestSnapshot' + Should -Invoke -CommandName 'New-SqlDscDatabase' -Exactly -Times 1 + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set ServerObject' -ForEach @( + @{ + ExpectedParameterSetName = 'ServerObject' + ExpectedParameters = '-ServerObject -Name -DatabaseName [-FileGroup ] [-Force] [-Refresh] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscDatabaseSnapshot').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 -Name [-FileGroup ] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscDatabaseSnapshot').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 in ServerObject parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDatabaseSnapshot').Parameters['ServerObject'] + $serverObjectSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'ServerObject' } + $serverObjectSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have DatabaseObject as a mandatory parameter in DatabaseObject parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDatabaseSnapshot').Parameters['DatabaseObject'] + $databaseObjectSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'DatabaseObject' } + $databaseObjectSetAttribute.Mandatory | Should -BeTrue + } + + It 'Should have Name as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDatabaseSnapshot').Parameters['Name'] + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have DatabaseName as a mandatory parameter in ServerObject parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscDatabaseSnapshot').Parameters['DatabaseName'] + $serverObjectSetAttribute = $parameterInfo.Attributes | Where-Object { $_.ParameterSetName -eq 'ServerObject' } + $serverObjectSetAttribute.Mandatory | Should -BeTrue + } + } +} diff --git a/tests/Unit/Public/New-SqlDscFileGroup.Tests.ps1 b/tests/Unit/Public/New-SqlDscFileGroup.Tests.ps1 new file mode 100644 index 0000000000..6bfc0dc156 --- /dev/null +++ b/tests/Unit/Public/New-SqlDscFileGroup.Tests.ps1 @@ -0,0 +1,428 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'New-SqlDscFileGroup' -Tag 'Public' { + Context 'When creating a new FileGroup with a Database' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject.Name = 'TestDatabase' + $mockDatabaseObject.Parent = $mockServerObject + } + + It 'Should create a FileGroup successfully' { + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -Name 'MyFileGroup' -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'MyFileGroup' + $result.Parent | Should -Be $mockDatabaseObject + } + + It 'Should create a PRIMARY FileGroup successfully' { + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -Name 'PRIMARY' -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'PRIMARY' + $result.Parent | Should -Be $mockDatabaseObject + } + + It 'Should support Force parameter to bypass confirmation' { + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -Name 'ForcedFileGroup' -Force + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'ForcedFileGroup' + $result.Parent | Should -Be $mockDatabaseObject + } + + It 'Should throw terminating error when Database object has no Parent property set' { + $mockDatabaseWithoutParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseWithoutParent.Name = 'TestDatabaseNoParent' + + { New-SqlDscFileGroup -Database $mockDatabaseWithoutParent -Name 'InvalidFileGroup' -Confirm:$false } | + Should -Throw -ExpectedMessage '*must have a Server object attached to the Parent property*' -ErrorId 'NSDFG0003,New-SqlDscFileGroup' + } + + It 'Should return null when WhatIf is specified' { + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -Name 'WhatIfFileGroup' -WhatIf + + $result | Should -BeNullOrEmpty + } + } + + Context 'When creating a standalone FileGroup' { + It 'Should create a standalone FileGroup without a Database' { + $result = New-SqlDscFileGroup -Name 'StandaloneFileGroup' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'StandaloneFileGroup' + $result.Parent | Should -BeNullOrEmpty + } + + It 'Should create a standalone PRIMARY FileGroup' { + $result = New-SqlDscFileGroup -Name 'PRIMARY' + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'PRIMARY' + $result.Parent | Should -BeNullOrEmpty + } + } + + Context 'When creating a FileGroup specification using AsSpec' { + It 'Should create a DatabaseFileGroupSpec object' { + InModuleScope -ScriptBlock { + $result = New-SqlDscFileGroup -Name 'MyFileGroup' -AsSpec + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $result.Name | Should -Be 'MyFileGroup' + $result.Files | Should -BeNullOrEmpty + $result.ReadOnly | Should -BeFalse + $result.IsDefault | Should -BeFalse + } + } + + It 'Should create a DatabaseFileGroupSpec with ReadOnly property set' { + InModuleScope -ScriptBlock { + $result = New-SqlDscFileGroup -Name 'ReadOnlyFileGroup' -AsSpec -ReadOnly + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $result.Name | Should -Be 'ReadOnlyFileGroup' + $result.ReadOnly | Should -BeTrue + $result.IsDefault | Should -BeFalse + } + } + + It 'Should create a DatabaseFileGroupSpec with IsDefault property set' { + InModuleScope -ScriptBlock { + $result = New-SqlDscFileGroup -Name 'PRIMARY' -AsSpec -IsDefault + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $result.Name | Should -Be 'PRIMARY' + $result.IsDefault | Should -BeTrue + $result.ReadOnly | Should -BeFalse + } + } + + It 'Should create a DatabaseFileGroupSpec with Files property set' { + InModuleScope -ScriptBlock { + # Create mock DatabaseFileSpec objects + $mockFileSpec1 = [DatabaseFileSpec]::new() + $mockFileSpec1.Name = 'TestFile1' + $mockFileSpec1.FileName = 'C:\SQLData\TestFile1.ndf' + + $mockFileSpec2 = [DatabaseFileSpec]::new() + $mockFileSpec2.Name = 'TestFile2' + $mockFileSpec2.FileName = 'C:\SQLData\TestFile2.ndf' + + $result = New-SqlDscFileGroup -Name 'DataFileGroup' -AsSpec -Files @($mockFileSpec1, $mockFileSpec2) + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $result.Name | Should -Be 'DataFileGroup' + $result.Files | Should -HaveCount 2 + $result.Files[0].Name | Should -Be 'TestFile1' + $result.Files[1].Name | Should -Be 'TestFile2' + } + } + + It 'Should create a DatabaseFileGroupSpec with all properties set' { + InModuleScope -ScriptBlock { + $mockFileSpec = [DatabaseFileSpec]::new() + $mockFileSpec.Name = 'PrimaryFile' + $mockFileSpec.FileName = 'C:\SQLData\PrimaryFile.mdf' + + $result = New-SqlDscFileGroup -Name 'PRIMARY' -AsSpec -Files @($mockFileSpec) -IsDefault -ReadOnly + + $result | Should -Not -BeNullOrEmpty + $result.GetType().Name | Should -Be 'DatabaseFileGroupSpec' + $result.Name | Should -Be 'PRIMARY' + $result.Files | Should -HaveCount 1 + $result.IsDefault | Should -BeTrue + $result.ReadOnly | Should -BeTrue + } + } + } + + Context 'When creating a FileGroup from a FileGroupSpec with Database' { + BeforeAll { + $mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $mockServerObject.InstanceName = 'TestInstance' + + $mockDatabaseObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseObject.Name = 'TestDatabase' + $mockDatabaseObject.Parent = $mockServerObject + + # Mock ConvertTo-SqlDscFileGroup + Mock -CommandName ConvertTo-SqlDscFileGroup -MockWith { + $fileGroup = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.FileGroup' -ArgumentList $DatabaseObject, $FileGroupSpec.Name + $fileGroup.ReadOnly = $FileGroupSpec.ReadOnly + $fileGroup.IsDefault = $FileGroupSpec.IsDefault + return $fileGroup + } + } + + It 'Should create a FileGroup from a FileGroupSpec object' { + $mockFileGroupSpec = InModuleScope -ScriptBlock { + $spec = [DatabaseFileGroupSpec]::new() + $spec.Name = 'SpecFileGroup' + $spec.ReadOnly = $false + $spec.IsDefault = $false + return $spec + } + + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -FileGroupSpec $mockFileGroupSpec -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'SpecFileGroup' + $result.Parent | Should -Be $mockDatabaseObject + + Should -Invoke -CommandName ConvertTo-SqlDscFileGroup -ParameterFilter { + $DatabaseObject -eq $mockDatabaseObject -and $FileGroupSpec.Name -eq 'SpecFileGroup' + } -Exactly -Times 1 -Scope It + } + + It 'Should create a FileGroup from a FileGroupSpec with properties set' { + $mockFileGroupSpec = InModuleScope -ScriptBlock { + $spec = [DatabaseFileGroupSpec]::new() + $spec.Name = 'ReadOnlySpec' + $spec.ReadOnly = $true + $spec.IsDefault = $false + return $spec + } + + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -FileGroupSpec $mockFileGroupSpec -Force + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'Microsoft.SqlServer.Management.Smo.FileGroup' + $result.Name | Should -Be 'ReadOnlySpec' + $result.ReadOnly | Should -BeTrue + + Should -Invoke -CommandName ConvertTo-SqlDscFileGroup -ParameterFilter { + $DatabaseObject -eq $mockDatabaseObject -and $FileGroupSpec.ReadOnly -eq $true + } -Exactly -Times 1 -Scope It + } + + It 'Should throw terminating error when Database object has no Parent property set' { + $mockDatabaseWithoutParent = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Database' + $mockDatabaseWithoutParent.Name = 'TestDatabaseNoParent' + + $mockFileGroupSpec = InModuleScope -ScriptBlock { + $spec = [DatabaseFileGroupSpec]::new() + $spec.Name = 'FailFileGroup' + return $spec + } + + { New-SqlDscFileGroup -Database $mockDatabaseWithoutParent -FileGroupSpec $mockFileGroupSpec -Confirm:$false } | + Should -Throw -ExpectedMessage '*must have a Server object attached to the Parent property*' -ErrorId 'NSDFG0003,New-SqlDscFileGroup' + } + + It 'Should return null when WhatIf is specified' { + $mockFileGroupSpec = InModuleScope -ScriptBlock { + $spec = [DatabaseFileGroupSpec]::new() + $spec.Name = 'WhatIfSpec' + return $spec + } + + $result = New-SqlDscFileGroup -Database $mockDatabaseObject -FileGroupSpec $mockFileGroupSpec -WhatIf + + $result | Should -BeNullOrEmpty + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'WithDatabase' + ExpectedParameters = '-Database -Name [-Force] [-WhatIf] [-Confirm] []' + } + @{ + ExpectedParameterSetName = 'WithDatabaseFromSpec' + ExpectedParameters = '-Database -FileGroupSpec [-Force] [-WhatIf] [-Confirm] []' + } + @{ + ExpectedParameterSetName = 'AsSpec' + ExpectedParameters = '-Name -AsSpec [-Files ] [-ReadOnly] [-IsDefault] [-WhatIf] [-Confirm] []' + } + @{ + ExpectedParameterSetName = 'Standalone' + ExpectedParameters = '-Name [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'New-SqlDscFileGroup').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 Database as a mandatory parameter in WithDatabase parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Database'] + $parameterSetInfo = $parameterInfo.ParameterSets['WithDatabase'] + $parameterSetInfo.IsMandatory | Should -BeTrue + } + + It 'Should have Database as a mandatory parameter in WithDatabaseFromSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Database'] + $parameterSetInfo = $parameterInfo.ParameterSets['WithDatabaseFromSpec'] + $parameterSetInfo.IsMandatory | Should -BeTrue + } + + It 'Should have Database parameter not be in Standalone parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Database'] + $parameterInfo.ParameterSets.Keys | Should -Not -Contain 'Standalone' + } + + It 'Should have Database parameter not be in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Database'] + $parameterInfo.ParameterSets.Keys | Should -Not -Contain 'AsSpec' + } + + It 'Should have Name as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Name'] + $parameterInfo.Attributes.Mandatory | Should -Contain $true + } + + It 'Should have Name parameter in WithDatabase parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Name'] + $parameterInfo.ParameterSets.Keys | Should -Contain 'WithDatabase' + } + + It 'Should have Name parameter in Standalone parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Name'] + $parameterInfo.ParameterSets.Keys | Should -Contain 'Standalone' + } + + It 'Should have Name parameter in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Name'] + $parameterInfo.ParameterSets.Keys | Should -Contain 'AsSpec' + } + + It 'Should have Name parameter not be in WithDatabaseFromSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Name'] + $parameterInfo.ParameterSets.Keys | Should -Not -Contain 'WithDatabaseFromSpec' + } + + It 'Should have FileGroupSpec as a mandatory parameter in WithDatabaseFromSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['FileGroupSpec'] + $parameterSetInfo = $parameterInfo.ParameterSets['WithDatabaseFromSpec'] + $parameterSetInfo.IsMandatory | Should -BeTrue + } + + It 'Should have AsSpec as a mandatory parameter in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['AsSpec'] + $parameterSetInfo = $parameterInfo.ParameterSets['AsSpec'] + $parameterSetInfo.IsMandatory | Should -BeTrue + } + + It 'Should have Files parameter only in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Files'] + $parameterInfo.ParameterSets.Keys | Should -Contain 'AsSpec' + $parameterInfo.ParameterSets.Keys | Should -HaveCount 1 + } + + It 'Should have ReadOnly parameter only in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['ReadOnly'] + $parameterInfo.ParameterSets.Keys | Should -Contain 'AsSpec' + $parameterInfo.ParameterSets.Keys | Should -HaveCount 1 + } + + It 'Should have IsDefault parameter only in AsSpec parameter set' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['IsDefault'] + $parameterInfo.ParameterSets.Keys | Should -Contain 'AsSpec' + $parameterInfo.ParameterSets.Keys | Should -HaveCount 1 + } + + It 'Should have four parameter sets (WithDatabase, WithDatabaseFromSpec, AsSpec, Standalone)' { + $command = Get-Command -Name 'New-SqlDscFileGroup' + $command.ParameterSets.Count | Should -Be 4 + $command.ParameterSets.Name | Should -Contain 'WithDatabase' + $command.ParameterSets.Name | Should -Contain 'WithDatabaseFromSpec' + $command.ParameterSets.Name | Should -Contain 'AsSpec' + $command.ParameterSets.Name | Should -Contain 'Standalone' + } + + It 'Should have Standalone as the default parameter set' { + $command = Get-Command -Name 'New-SqlDscFileGroup' + $command.DefaultParameterSet | Should -Be 'Standalone' + } + + It 'Should support ShouldProcess' { + $command = Get-Command -Name 'New-SqlDscFileGroup' + $command.Parameters.ContainsKey('WhatIf') | Should -BeTrue + $command.Parameters.ContainsKey('Confirm') | Should -BeTrue + } + + It 'Should have Force parameter only in WithDatabase and WithDatabaseFromSpec parameter sets' { + $parameterInfo = (Get-Command -Name 'New-SqlDscFileGroup').Parameters['Force'] + $parameterInfo | Should -Not -BeNullOrEmpty + $parameterInfo.ParameterSets.Keys | Should -Contain 'WithDatabase' + $parameterInfo.ParameterSets.Keys | Should -Contain 'WithDatabaseFromSpec' + $parameterInfo.ParameterSets.Keys | Should -HaveCount 2 + } + + It 'Should have ConfirmImpact set to High' { + $command = Get-Command -Name 'New-SqlDscFileGroup' + $command.ScriptBlock.Attributes | Where-Object { $_.TypeId.Name -eq 'CmdletBindingAttribute' } | + ForEach-Object { $_.ConfirmImpact } | Should -Be 'High' + } + } +} diff --git a/tests/Unit/Stubs/SMO.cs b/tests/Unit/Stubs/SMO.cs index e78f98c061..40ab973809 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -877,21 +877,29 @@ public class Database public DateTime CreateDate; public DatabaseEncryptionKey DatabaseEncryptionKey; public DateTime LastBackupDate = DateTime.Now; - public Hashtable FileGroups; + public FileGroupCollection FileGroups { get; set; } public Hashtable LogFiles; + public string DatabaseSnapshotBaseName; - public Database( Server server, string name ) { + public Database( Server server, string name ) + { this.Name = name; this.Parent = server; + this.FileGroups = new FileGroupCollection(); } - public Database( Object server, string name ) { + public Database( Object server, string name ) + { this.Name = name; this.Parent = (Server)server; + this.FileGroups = new FileGroupCollection(); } - public Database() {} + public Database() + { + this.FileGroups = new FileGroupCollection(); + } public string Name; public Server Parent; @@ -961,6 +969,135 @@ public void SetDefaultFullTextCatalog( string catalogName ) } } + // TypeName: Microsoft.SqlServer.Management.Smo.FileGroup + // Used by: + // New-SqlDscDatabase.Tests.ps1 + // New-SqlDscDatabaseSnapshot.Tests.ps1 + public class FileGroup + { + public FileGroup() + { + this.Files = new DataFileCollection(); + } + + public FileGroup(Database database) + { + this.Parent = database; + this.Files = new DataFileCollection(); + } + + public FileGroup(Database database, string name) + { + this.Parent = database; + this.Name = name; + this.Files = new DataFileCollection(); + } + + public string Name { get; set; } + public Database Parent { get; set; } + public DataFileCollection Files { get; set; } + public bool ReadOnly { get; set; } + public bool IsDefault { get; set; } + + public void Create() + { + } + } + + // TypeName: Microsoft.SqlServer.Management.Smo.FileGroupCollection + // Used by: + // New-SqlDscDatabase.Tests.ps1 + // New-SqlDscDatabaseSnapshot.Tests.ps1 + public class FileGroupCollection : Collection + { + public FileGroup this[string name] + { + get + { + foreach (FileGroup fileGroup in this) + { + if (name == fileGroup.Name) + { + return fileGroup; + } + } + + return null; + } + } + + new public void Add(FileGroup fileGroup) + { + base.Add(fileGroup); + } + } + + // TypeName: Microsoft.SqlServer.Management.Smo.DataFile + // Used by: + // New-SqlDscDatabase.Tests.ps1 + // New-SqlDscDatabaseSnapshot.Tests.ps1 + // New-SqlDscDataFile.Tests.ps1 + public class DataFile + { + public DataFile() + { + } + + public DataFile(FileGroup fileGroup, string name) + { + this.Parent = fileGroup; + this.Name = name; + } + + public DataFile(FileGroup fileGroup, string name, string fileName) + { + this.Parent = fileGroup; + this.Name = name; + this.FileName = fileName; + } + + public string Name { get; set; } + public string FileName { get; set; } + public FileGroup Parent { get; set; } + public double Size { get; set; } + public double MaxSize { get; set; } + public double Growth { get; set; } + public string GrowthType { get; set; } + public bool IsPrimaryFile { get; set; } + + public void Create() + { + } + } + + // TypeName: Microsoft.SqlServer.Management.Smo.DataFileCollection + // Used by: + // New-SqlDscDatabase.Tests.ps1 + // New-SqlDscDatabaseSnapshot.Tests.ps1 + public class DataFileCollection : Collection + { + public DataFile this[string name] + { + get + { + foreach (DataFile dataFile in this) + { + if (name == dataFile.Name) + { + return dataFile; + } + } + + return null; + } + } + + new public void Add(DataFile dataFile) + { + base.Add(dataFile); + } + } + // TypeName: Microsoft.SqlServer.Management.Smo.User // BaseType: Microsoft.SqlServer.Management.Smo.ScriptNameObjectBase // Used by: