diff --git a/.MetaTestOptIn.json b/.MetaTestOptIn.json index 6e2168cb2..146c01c2a 100644 --- a/.MetaTestOptIn.json +++ b/.MetaTestOptIn.json @@ -5,5 +5,7 @@ "Common Tests - Validate Example Files", "Common Tests - Required Script Analyzer Rules", "Common Tests - New Error-Level Script Analyzer Rules", - "Common Tests - Custom Script Analyzer Rules" + "Common Tests - Custom Script Analyzer Rules", + "Common Tests - Validate Markdown Links", + "Common Tests - Relative Path Length" ] diff --git a/.gitignore b/.gitignore index c333d23d5..6094d6df2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -DSCResource.Tests +DscResource.Tests +DSCResource.Tests .vs .vscode node_modules diff --git a/.vscode/analyzersettings.psd1 b/.vscode/analyzersettings.psd1 new file mode 100644 index 000000000..be415e4d5 --- /dev/null +++ b/.vscode/analyzersettings.psd1 @@ -0,0 +1,53 @@ +@{ + <# + For the custom rules to work, the DscResource.Tests repo must be + cloned. It is automatically clone as soon as any unit or + integration tests are run. + #> + CustomRulePath = '.\DSCResource.Tests\DscResource.AnalyzerRules' + + IncludeRules = @( + # DSC Resource Kit style guideline rules. + 'PSAvoidDefaultValueForMandatoryParameter', + 'PSAvoidDefaultValueSwitchParameter', + 'PSAvoidInvokingEmptyMembers', + 'PSAvoidNullOrEmptyHelpMessageAttribute', + 'PSAvoidUsingCmdletAliases', + 'PSAvoidUsingComputerNameHardcoded', + 'PSAvoidUsingDeprecatedManifestFields', + 'PSAvoidUsingEmptyCatchBlock', + 'PSAvoidUsingInvokeExpression', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidShouldContinueWithoutForce', + 'PSAvoidUsingWMICmdlet', + 'PSAvoidUsingWriteHost', + 'PSDSCReturnCorrectTypesForDSCFunctions', + 'PSDSCStandardDSCFunctionsInResource', + 'PSDSCUseIdenticalMandatoryParametersForDSC', + 'PSDSCUseIdenticalParametersForDSC', + 'PSMisleadingBacktick', + 'PSMissingModuleManifestField', + 'PSPossibleIncorrectComparisonWithNull', + 'PSProvideCommentHelp', + 'PSReservedCmdletChar', + 'PSReservedParams', + 'PSUseApprovedVerbs', + 'PSUseCmdletCorrectly', + 'PSUseOutputTypeCorrectly', + 'PSAvoidGlobalVars', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + 'PSAvoidUsingPlainTextForPassword', + 'PSAvoidUsingUsernameAndPasswordParams', + 'PSDSCUseVerboseMessageInDSCResource', + 'PSShouldProcess', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSUsePSCredentialType', + + <# + This is to test all the DSC Resource Kit custom rules. + The name of the function-blocks of each custom rule start + with 'Measure*'. + #> + 'Measure-*' + ) +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 87c4308ca..0969e57b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "powershell.codeFormatting.ignoreOneLineBlock": false, "powershell.codeFormatting.preset": "Custom", "files.trimTrailingWhitespace": true, - "files.insertFinalNewline": true + "files.insertFinalNewline": true, + "powershell.scriptAnalysis.settingsPath": ".vscode\\analyzersettings.psd1" } diff --git a/Assert-TestEnvironment.ps1 b/Assert-TestEnvironment.ps1 new file mode 100644 index 000000000..f587014c7 --- /dev/null +++ b/Assert-TestEnvironment.ps1 @@ -0,0 +1,103 @@ +<# + .SYNOPSIS + Assert that the test environment is properly setup and loaded into the + PowerShell session. + + .DESCRIPTION + Assert that the test environment is properly setup and loaded into the + PowerShell session. + + .EXAMPLE + .\Assert-testEnvironment.ps1 + + Will assert that the current PowerShell session is ready to run tests. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param +( +) + +#region Verify prerequisites Pester +$pesterModuleName = 'Pester' + +# This is the minimum version that can be used with the tests in this repo. +$pesterModuleMinimumVersion = '4.0.2' + +<# + Pester v4.4.0 has a fix for '-Not -Throw' so it shows the actual error + message if an unexpected exception does occur. It will help when debugging + tests. + If no Pester module exist, then use this as the minimum version. +#> +$pesterModuleRecommendedMinimumVersion = '4.4.0' + +$pesterModule = Get-Module $pesterModuleName -ListAvailable -Verbose:$false | + Where-Object -Property 'Version' -GE -Value $pesterModuleMinimumVersion | + Sort-Object -Property 'Version' -Descending | + Select-Object -First 1 + +if (-not $pesterModule) +{ + <# + Not installing the module here because it's not known what scope the + user want (can) to install the module in. + #> + $message = 'Missing a compatible version of the {0} module. Minimum version of {0} module can be ''{2}'', but the recommended minimum version is ''{1}''.' -f $pesterModuleName, $pesterModuleRecommendedMinimumVersion, $pesterModuleMinimumVersion + Write-Warning -Message $message + $dependencyMissing = $true +} +else +{ + Write-Verbose -Message ('A compatible {0} module is already installed (v{1}). If you want to use a newer version of {0} module, please install it manually.' -f $pesterModule.Name, $pesterModule.Version) +} +#endregion Verify prerequisites Pester + +#region Verify prerequisites PSDepend +$psDependModuleName = 'PSDepend' + +# This is the minimum version that can be used with the tests in this repo. +$psDependModuleMinimumVersion = '0.3.0' +$psDependModuleRecommendedMinimumVersion = 'latest' + +$psDependModule = Get-Module $psDependModuleName -ListAvailable -Verbose:$false | + Where-Object -Property 'Version' -GE -Value $psDependModuleMinimumVersion | + Sort-Object -Property 'Version' -Descending | + Select-Object -First 1 + +if (-not $psDependModule) +{ + <# + Not installing the module here because it's not known what scope the + user want (can) to install the module in. + #> + $message = 'Missing a compatible version of the {0} module. Minimum version of {0} module can be ''{2}'', but the recommended minimum version is ''{1}''. Please install {0} module manually, then run this script again.' -f $psDependModuleName, $psDependModuleRecommendedMinimumVersion, $psDependModuleMinimumVersion + Write-Warning -Message $message + $dependencyMissing = $true +} +else +{ + Write-Verbose -Message ('A compatible {0} module is already installed (v{1}). If you want to use a newer version of {0} module, please install it manually.' -f $psDependModule.Name, $psDependModule.Version) +} +#endregion Verify prerequisites PSDepend + +if ($dependencyMissing) +{ + Write-Output -InputObject 'Please install the necessary dependencies manually, then run this script again.' + return +} + +$dependenciesPath = Join-Path $PSScriptRoot -ChildPath 'Tests' + +Write-Verbose -Message ('Running Invoke-PSDepend using dependencies found under the path ''{0}''.' -f $dependenciesPath) + +if ($PSBoundParameters.ContainsKey('Confirm')) +{ + $invokePSDependConfirmation = $ConfirmPreference +} +else +{ + $invokePSDependConfirmation = $false +} + +Invoke-PSDepend -Path $dependenciesPath -Confirm:$invokePSDependConfirmation diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8856b6f..524442d19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,61 @@ ## Unreleased +## 12.1.0.0 + +- Changes to SqlServerDsc + - Add support for validating the code with the DSC ResourceKit + Script Analyzer rules, both in Visual Studio Code and directly using + `Invoke-ScriptAnalyzer`. + - Opt-in for common test "Common Tests - Validate Markdown Links". + - Updated broken links in `\README.md` and in `\Examples\README.md` + - Opt-in for common test 'Common Tests - Relative Path Length'. + - Updated the Installation section in the README.md. + - Updated the Contributing section in the README.md after + [Style Guideline and Best Practices guidelines](https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md) + has merged into one document. + - To speed up testing in AppVeyor, unit tests are now run in two containers. + - Adding the PowerShell script `Assert-TestEnvironment.ps1` which + must be run prior to running any unit tests locally with + `Invoke-Pester`. + Read more in the specific contributing guidelines, under the section + [Unit Tests](https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#unit-tests). +- Changes to SqlServerDscHelper + - Fix style guideline lint errors. + - Changes to Connect-SQL + - Adding verbose message in Connect-SQL so it + now shows the username that is connecting. + - Changes to Import-SQLPS + - Fixed so that when importing SQLPS it imports + using the path (and not the .psd1 file). + - Fixed so that the verbose message correctly + shows the name, version and path when importing + the module SQLPS (it did show correctly for the + SqlServer module). +- Changes to SqlAg, SqlAGDatabase, and SqlAGReplica examples + - Included configuration for SqlAlwaysOnService to enable + HADR on each node to avoid confusion + ([issue #1182](https://github.com/PowerShell/SqlServerDsc/issues/1182)). +- Changes to SqlServerDatabaseMail + - Minor update to Ensure parameter description in the README.md. +- Changes to Write-ModuleStubFile.ps1 + - Create aliases for cmdlets in the stubbed module which have aliases + ([issue #1224](https://github.com/PowerShell/SqlServerDsc/issues/1224)). + [Dan Reist (@randomnote1)](https://github.com/randomnote1) + - Use a string builder to build the function stubs. + - Fixed formatting issues for the function to work with modules other + than SqlServer. +- New DSC resource SqlServerSecureConnection + - New resource to configure a SQL Server instance for encrypted SQL + connections. +- Changes to SqlAlwaysOnService + - Updated integration tests to use NetworkingDsc + ([issue #1129](https://github.com/PowerShell/SqlServerDsc/issues/1129)). +- Changes to SqlServiceAccount + - Fix unit tests that didn't mock some of the calls. It no longer fail + when a SQL Server installation is not present on the node running the + unit test ([issue #983](https://github.com/PowerShell/SqlServerDsc/issues/983)). + ## 12.0.0.0 - Changes to SqlServerDatabaseMail diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0f07b6394..c6210f3f5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -378,14 +378,23 @@ For a review of a Pull Request (PR) to start, all tests must pass without error. If you need help to figure why some test don't pass, just write a comment in the Pull Request (PR), or submit an issue, and somebody will come along and assist. -To run all tests manually run the following. +To run all unit tests manually run the following. ```powershell -Install-Module Pester -cd '\Tests' +cd '' +.\Assert-TestEnvironment.ps1 -Confirm -Verbose +cd '\Tests\Unit' Invoke-Pester ``` +The script `Assert-TestEnvironment.ps1` will clone the test framework +from GitHub, and load some necessary types which is needed to run some +of the tests. Read more about the bootstrap script in the section +[Bootstrap script Assert-TestEnvironment](#bootstrap-script-assert-testenvironment). + +The cmdlet `Invoke-Pester` looks for all the '*.Tests.ps1' PowerShell +script files recursively and executes the tests. + #### Unit tests for style check of Markdown files When sending in a Pull Request (PR) a style check will be performed on all Markdown @@ -439,3 +448,29 @@ If using Visual Studio Code to edit Markdown files it can be a good idea to inst the markdownlint extension. It will help to do style checking. The file [.markdownlint.json](/.markdownlint.json) is prepared with a default set of rules which will automatically be used by the extension. + +## Bootstrap script Assert-TestEnvironment + +The bootstrap script [`Assert-TestEnvironment.ps1`](Assert-TestEnvironment.ps1) +was needed when tests were updated to run in containers. +There are some custom types (`Microsoft.DscResourceKit.*`) that are needed +to be able to parse the test script files. Those types must be loaded +into the session prior to running any unit tests with `Invoke-Pester`. + +The script works without any parameters and will do the following. + +>**Note:** If you want to confirm each step, then add the `-Confirm` +>parameter. + +- Check if a compatible *Pester* version is available. +- Check if a compatible *PSDepend* version is available. +- Invoke PSDepend (using `Tests/Tests.depend.psd1`). + - Remove local cloned repository *DscResource.Tests* if it is present + (always assumes it's an older version). + - Clone *DscResource.Tests* from GitHub into the local repository + folder, and check out the branch `dev` to always be on the latest + commit available. + - Load the necessary types from the test framework *DscResource.Tests*. + +If there are no compatible Pester or PSDepend version available, you will be asked +to install it manually, and then run the script again. diff --git a/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.psm1 b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.psm1 new file mode 100644 index 000000000..e14fd931c --- /dev/null +++ b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.psm1 @@ -0,0 +1,568 @@ +Import-Module -Name (Join-Path -Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) ` + -ChildPath 'SqlServerDscHelper.psm1') -Force + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_SqlServerSecureConnection' + +<# + .SYNOPSIS + Gets the SQL Server Encryption status. + + .PARAMETER InstanceName + Name of the SQL Server instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Thumbprint can be set to an empty string. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is that all connections must be encrypted. + + .PARAMETER Ensure + If Encryption should be Enabled (Present) or Disabled (Absent). + + .PARAMETER ServiceAccount + Name of the account running the SQL Server service. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $Thumbprint, + + [Parameter()] + [System.Boolean] + $ForceEncryption = $true, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceAccount + ) + + Write-Verbose -Message ( + $script:localizedData.GetEncryptionSettings ` + -f $InstanceName + ) + + $encryptionSettings = Get-EncryptedConnectionSetting -InstanceName $InstanceName + + Write-Verbose -Message ( + $script:localizedData.EncryptedSettings ` + -f $encryptionSettings.Certificate, $encryptionSettings.ForceEncryption + ) + + if ($Ensure -eq 'Present') + { + $ensureValue = 'Present' + $certificateSettings = Test-CertificatePermission -Thumbprint $Thumbprint -ServiceAccount $ServiceAccount + if ($encryptionSettings.Certificate -ine $Thumbprint) + { + Write-Verbose -Message ( + $script:localizedData.ThumbprintResult ` + -f $encryptionSettings.Certificate, $Thumbprint + ) + $ensureValue = 'Absent' + } + + if ($encryptionSettings.ForceEncryption -ne $ForceEncryption) + { + Write-Verbose -Message ( + $script:localizedData.ForceEncryptionResult ` + -f $encryptionSettings.ForceEncryption, $ForceEncryption + ) + $ensureValue = 'Absent' + } + + if (-not $certificateSettings) + { + Write-Verbose -Message ( + $script:localizedData.CertificateSettings ` + -f 'Configured' + ) + + $ensureValue = 'Absent' + } + else + { + Write-Verbose -Message ( + $script:localizedData.CertificateSettings ` + -f 'Not Configured' + ) + + } + } + else + { + $ensureValue = 'Absent' + if ($encryptionSettings.ForceEncryption -eq $false) + { + Write-Verbose -Message ( + $script:localizedData.EncryptionOff + ) + } + else + { + $ensureValue = 'Present' + Write-Verbose -Message ( + $script:localizedData.ForceEncryptionResult ` + -f $encryptionSettings.ForceEncryption, $false + ) + } + + if ($encryptionSettings.Certificate -eq '') + { + $certificateValue = 'Empty' + } + else + { + $ensureValue = 'Present' + Write-Verbose -Message ( + $script:localizedData.ThumbprintResult ` + -f $encryptionSettings.Certificate, 'Empty' + ) + $certificateValue = $encryptionSettings.Certificate + } + Write-Verbose -Message ( + $script:localizedData.EncryptedSettings ` + -f $certificateValue, $encryptionSettings.ForceEncryption + ) + } + + return @{ + InstanceName = [System.String] $InstanceName + Thumbprint = [System.String] $encryptionSettings.Certificate + ForceEncryption = [System.Boolean] $encryptionSettings.ForceEncryption + Ensure = [System.String] $ensureValue + ServiceAccount = [System.String] $ServiceAccount + } +} + +<# + .SYNOPSIS + Enables SQL Server Encryption Connection. + + .PARAMETER InstanceName + Name of the SQL Server instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Thumbprint can be set to an empty string. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is that all connections must be encrypted. + + .PARAMETER Ensure + If Encryption should be Enabled (Present) or Disabled (Absent). + + .PARAMETER ServiceAccount + Name of the account running the SQL Server service. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $Thumbprint, + + [Parameter()] + [System.Boolean] + $ForceEncryption = $true, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceAccount + ) + + $parameters = @{ + InstanceName = $InstanceName + Thumbprint = $Thumbprint + ForceEncryption = $ForceEncryption + Ensure = $Ensure + ServiceAccount = $ServiceAccount + } + + $encryptionState = Get-TargetResource @parameters + + if ($Ensure -eq 'Present') + { + if ($ForceEncryption -ne $encryptionState.ForceEncryption -or $Thumbprint -ne $encryptionState.Thumbprint) + { + Write-Verbose -Message ( + $script:localizedData.SetEncryptionSetting ` + -f $InstanceName, $Thumbprint, $ForceEncryption + ) + Set-EncryptedConnectionSetting -InstanceName $InstanceName -Thumbprint $Thumbprint -ForceEncryption $ForceEncryption + } + + if ((Test-CertificatePermission -Thumbprint $Thumbprint -ServiceAccount $ServiceAccount) -eq $false) + { + Write-Verbose -Message ( + $script:localizedData.SetCertificatePermission ` + -f $Thumbprint, $ServiceAccount + ) + Set-CertificatePermission -Thumbprint $Thumbprint -ServiceAccount $ServiceAccount + } + } + else + { + Write-Verbose -Message ( + $script:localizedData.RemoveEncryptionSetting ` + -f $InstanceName + ) + Set-EncryptedConnectionSetting -InstanceName $InstanceName -Thumbprint '' -ForceEncryption $false + } + + Write-Verbose -Message ( + $script:localizedData.RestartingService ` + -f $InstanceName + ) + Restart-SqlService -SQLServer localhost -SQLInstanceName $InstanceName +} + +<# + .SYNOPSIS + Tests the SQL Server Encryption configuration. + + .PARAMETER InstanceName + Name of the SQL Server instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Thumbprint can be set to an empty string. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is, set to true, that all connections must be encrypted. + + .PARAMETER Ensure + If Encryption should be Enabled (Present) or Disabled (Absent). + + .PARAMETER ServiceAccount + Name of the account running the SQL Server service. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $Thumbprint, + + [Parameter()] + [System.Boolean] + $ForceEncryption = $true, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceAccount + ) + + $parameters = @{ + InstanceName = $InstanceName + Thumbprint = $Thumbprint + ForceEncryption = $ForceEncryption + Ensure = $Ensure + ServiceAccount = $ServiceAccount + } + + Write-Verbose -Message ( + $script:localizedData.TestingConfiguration ` + -f $InstanceName + ) + + $encryptionState = Get-TargetResource @parameters + + return $Ensure -eq $encryptionState.Ensure +} + +<# + .SYNOPSIS + Gets the SQL Server Encryption settings. Returns Certificate thumbprint and ForceEncryption setting. + + .PARAMETER InstanceName + Name of the SQL Server Instance to be configured. +#> +function Get-SqlEncryptionValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $InstanceName + ) + + $sqlInstance = Get-Item 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' + if ($sqlInstance) + { + try + { + $sqlInstanceId = (Get-ItemProperty -Path $sqlInstance.PSPath -Name $InstanceName).$InstanceName + } + catch + { + throw ($script:localizedData.InstanceNotFound -f $InstanceName) + } + return Get-Item "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$sqlInstanceId\MSSQLServer\SuperSocketNetLib" + } +} + +<# + .SYNOPSIS + Gets the SQL Server Encryption settings. Returns Certificate thumbprint and ForceEncryption setting. + + .PARAMETER InstanceName + Name of the SQL Server Instance to be configured. +#> +function Get-EncryptedConnectionSetting +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [string] + $InstanceName + ) + + $superSocketNetLib = Get-SqlEncryptionValue -InstanceName $InstanceName + if ($superSocketNetLib) + { + return @{ + ForceEncryption = [System.Boolean](Get-ItemProperty -Path $superSocketNetLib.PSPath -Name 'ForceEncryption').ForceEncryption + Certificate = (Get-ItemProperty -Path $superSocketNetLib.PSPath -Name 'Certificate').Certificate + } + } + return $null +} + +<# + .SYNOPSIS + Sets the SQL Server Encryption settings. + + .PARAMETER InstanceName + Name of the SQL Server Instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. +#> +function Set-EncryptedConnectionSetting +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $InstanceName, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] + $Thumbprint, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $ForceEncryption + ) + + $superSocketNetLib = Get-SqlEncryptionValue -InstanceName $InstanceName + if ($superSocketNetLib) + { + Set-ItemProperty -Path $superSocketNetLib.PSPath -Name 'Certificate' -Value $Thumbprint + Set-ItemProperty -Path $superSocketNetLib.PSPath -Name 'ForceEncryption' -Value $([int]$ForceEncryption) + } + else + { + throw $script:localizedData.CouldNotFindEncryptionValues ` + -f $InstanceName + } +} + +<# + .SYNOPSIS + Gets the permissions of the private key on the certificate. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. +#> + +function Get-CertificateAcl +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Thumbprint + ) + + $cert = Get-ChildItem -Path cert:\LocalMachine\My | Where-Object -FilterScript { $PSItem.Thumbprint -eq $Thumbprint } + + # Location of the machine related keys + $keyPath = $env:ProgramData + '\Microsoft\Crypto\RSA\MachineKeys\' + $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + $keyFullPath = $keyPath + $keyName + + Write-Verbose -Message ( + $script:localizedData.PrivateKeyPath ` + -f $keyFullPath + ) + + try + { + # Get the current acl of the private key + return @{ + ACL = (Get-Item $keyFullPath).GetAccessControl() + Path = $keyFullPath + } + } + catch + { + throw $_ + } +} + +<# + .SYNOPSIS + Gives the service account read permissions to the private key on the certificate. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. + + .PARAMETER ServiceAccount + The service account running SQL Server service. +#> +function Set-CertificatePermission +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Thumbprint, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $ServiceAccount + ) + + # Specify the user, the permissions and the permission type + $permission = "$($ServiceAccount)", 'Read', 'Allow' + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + + try + { + # Get the current acl of the private key + $acl = Get-CertificateAcl -Thumbprint $Thumbprint + + # Add the new ace to the acl of the private key + $acl.ACL.AddAccessRule($accessRule) + + # Write back the new acl + Set-Acl -Path $acl.Path -AclObject $acl.ACL + } + catch + { + throw $_ + } +} + +<# + .SYNOPSIS + Test if the service account has read permissions to the private key on the certificate. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. + + .PARAMETER ServiceAccount + The service account running SQL Server service. +#> +function Test-CertificatePermission +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Thumbprint, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $ServiceAccount + ) + + # Specify the user, the permissions and the permission type + $permission = "$($ServiceAccount)", 'Read', 'Allow' + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + + try + { + # Get the current acl of the private key + $acl = Get-CertificateAcl -Thumbprint $Thumbprint + + [array] $permissions = $acl.ACL.Access.Where( {$_.IdentityReference -eq $accessRule.IdentityReference}) + if ($permissions.Count -eq 0) + { + return $false + } + + $rights = $permissions[0].FileSystemRights.value__ + + #check if the rights contains Read permission, 131209 is the bitwise number for read. This allows the permissions to be higher then read. + if (($rights -bor 131209) -ne $rights) + { + return $false + } + + return $true + } + catch + { + return $false + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.schema.mof b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.schema.mof new file mode 100644 index 000000000..f05c80391 --- /dev/null +++ b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.schema.mof @@ -0,0 +1,11 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("SqlServerSecureConnection")] +class MSFT_SqlServerSecureConnection : OMI_BaseResource +{ + [Key, Description("Name of the SQL Server instance to be configured.")] String InstanceName; + [Required, Description("Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Certificate can be set to an empty string.")] String Thumbprint; + [Write, Description("If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is, set to true, that all connections must be encrypted.")] boolean ForceEncryption; + [Required, Description("Name of the account running the SQL Server service.")] String ServiceAccount; + [Write, Description("If Encryption should be Enabled (Present) or Disabled (Absent)."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; + diff --git a/DSCResources/MSFT_SqlServerSecureConnection/en-US/MSFT_SqlServerSecureConnection.strings.psd1 b/DSCResources/MSFT_SqlServerSecureConnection/en-US/MSFT_SqlServerSecureConnection.strings.psd1 new file mode 100644 index 000000000..9bbaf8d92 --- /dev/null +++ b/DSCResources/MSFT_SqlServerSecureConnection/en-US/MSFT_SqlServerSecureConnection.strings.psd1 @@ -0,0 +1,21 @@ +<# + Localized resources for MSFT_SqlServerSecureConnection +#> + +ConvertFrom-StringData @' + GetEncryptionSettings = Getting encryption settings for instance '{0}'. + CertificateSettings = Certificate permissions are {0}. + EncryptedSettings = Found thumbprint of '{0}', with Force Encryption set to '{1}'. + SetEncryptionSetting = Securing instance '{0}' with Thumbprint: '{1}' and Force Encryption: '{2}'. + RemoveEncryptionSetting = Removing SQL Server secure connection from instance '{0}'. + SetCertificatePermission = Adding read permissions to certificate '{0}' for account '{1}'. + RestartingService = Restarting SQL Server service for instance '{0}'. + TestingConfiguration = Determine if the Secure Connection is in the desired state. + ThumbprintResult = Thumbprint was '{0}' but expected '{1}'. + ForceEncryptionResult = ForceEncryption was '{0}' but expected '{1}'. + CertificateResult = Certificate permissions was '{0}' but expected 'True'. + EncryptionOff = SQL Secure Connection is Disabled. + InstanceNotFound = SQL instance '{0}' not found on SQL Server. + PrivateKeyPath = Certificate private key is located at '{0}'. + CouldNotFindEncryptionValues = Could not find encryption values in registry for instance '{0}'. +'@ diff --git a/Examples/README.md b/Examples/README.md index 31c6f7daf..64d5bc090 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -7,37 +7,38 @@ resource module. These are the links to the examples for each individual resource. -- [SqlAG](/Examples/Resources/SqlAG) -- [SqlAGDatabase](/Examples/Resources/SqlAGDatabase) -- [SqlAGListener](/Examples/Resources/SqlAGListener) -- [SqlAGReplica](/Examples/Resources/SqlAGReplica) -- [SqlAlias](/Examples/Resources/SqlAlias) -- [SqlAlwaysOnService](/Examples/Resources/SqlAlwaysOnService) -- [SqlDatabase](/Examples/Resources/SqlDatabase) -- [SqlDatabaseDefaultLocation](/Examples/Resources/SqlDatabaseDefaultLocation) -- [SqlDatabaseOwner](/Examples/Resources/SqlDatabaseOwner) -- [SqlDatabasePermission](/Examples/Resources/SqlDatabasePermission) -- [SqlDatabaseRecoveryModel](/Examples/Resources/SqlDatabaseRecoveryModel) -- [SqlDatabaseRole](/Examples/Resources/SqlDatabaseRole) -- [SqlRS](/Examples/Resources/SqlRS) -- [SqlScript](/Examples/Resources/SqlScript) -- [SqlScriptQuery](/Examples/Resources/SqlScriptQuery) -- [SqlServerConfiguration](/Examples/Resources/SqlServerConfiguration) -- [SqlServerDatabaseMail](/Examples/Resources/SqlServerDatabaseMail) -- [SqlServerEndpoint](/Examples/Resources/SqlServerEndpoint) -- [SqlServerEndpointPermission](/Examples/Resources/SqlServerEndpointPermission) -- [SqlServerEndpointState](/Examples/Resources/SqlServerEndpointState) -- [SqlServerLogin](/Examples/Resources/SqlServerLogin) -- [SqlServerMaxDop](/Examples/Resources/SqlServerMaxDop) -- [SqlServerMemory](/Examples/Resources/SqlServerMemory) -- [SqlServerNetwork](/Examples/Resources/SqlServerNetwork) -- [SqlServerPermission](/Examples/Resources/SqlServerPermission) -- [SqlServerReplication](/Examples/Resources/SqlServerReplication) -- [SqlServerRole](/Examples/Resources/SqlServerRole) -- [SqlServiceAccount](/Examples/Resources/SqlServiceAccount) -- [SqlSetup](/Examples/Resources/SqlSetup) -- [SqlWaitForAG](/Examples/Resources/SqlWaitForAG) -- [SqlWindowsFirewall](/Examples/Resources/SqlWindowsFirewall) +- [SqlAG](Resources/SqlAG) +- [SqlAGDatabase](Resources/SqlAGDatabase) +- [SqlAGListener](Resources/SqlAGListener) +- [SqlAGReplica](Resources/SqlAGReplica) +- [SqlAlias](Resources/SqlAlias) +- [SqlAlwaysOnService](Resources/SqlAlwaysOnService) +- [SqlDatabase](Resources/SqlDatabase) +- [SqlDatabaseDefaultLocation](Resources/SqlDatabaseDefaultLocation) +- [SqlDatabaseOwner](Resources/SqlDatabaseOwner) +- [SqlDatabasePermission](Resources/SqlDatabasePermission) +- [SqlDatabaseRecoveryModel](Resources/SqlDatabaseRecoveryModel) +- [SqlDatabaseRole](Resources/SqlDatabaseRole) +- [SqlRS](Resources/SqlRS) +- [SqlScript](Resources/SqlScript) +- [SqlScriptQuery](Resources/SqlScriptQuery) +- [SqlServerConfiguration](Resources/SqlServerConfiguration) +- [SqlServerDatabaseMail](Resources/SqlServerDatabaseMail) +- [SqlServerEndpoint](Resources/SqlServerEndpoint) +- [SqlServerEndpointPermission](Resources/SqlServerEndpointPermission) +- [SqlServerEndpointState](Resources/SqlServerEndpointState) +- [SqlServerLogin](Resources/SqlServerLogin) +- [SqlServerMaxDop](Resources/SqlServerMaxDop) +- [SqlServerMemory](Resources/SqlServerMemory) +- [SqlServerNetwork](Resources/SqlServerNetwork) +- [SqlServerPermission](Resources/SqlServerPermission) +- [SqlServerReplication](Resources/SqlServerReplication) +- [SqlServerRole](Resources/SqlServerRole) +- [SqlServerSecureConnection](Resources/SqlServerSecureConnection) +- [SqlServiceAccount](Resources/SqlServiceAccount) +- [SqlSetup](Resources/SqlSetup) +- [SqlWaitForAG](Resources/SqlWaitForAG) +- [SqlWindowsFirewall](Resources/SqlWindowsFirewall) ## Setting up a SQL Server Failover Cluster diff --git a/Examples/Resources/SqlAG/1-CreateAvailabilityGroup.ps1 b/Examples/Resources/SqlAG/1-CreateAvailabilityGroup.ps1 index 728e85fd8..9f9a91b51 100644 --- a/Examples/Resources/SqlAG/1-CreateAvailabilityGroup.ps1 +++ b/Examples/Resources/SqlAG/1-CreateAvailabilityGroup.ps1 @@ -27,7 +27,8 @@ Configuration Example Import-DscResource -ModuleName SqlServerDsc - Node $AllNodes.NodeName { + Node $AllNodes.NodeName + { # Adding the required service account to allow the cluster to log into SQL SqlServerLogin AddNTServiceClusSvc { @@ -62,6 +63,15 @@ Configuration Example PsDscRunAsCredential = $SqlAdministratorCredential } + # Ensure the HADR option is enabled for the instance + SqlAlwaysOnService EnableHADR + { + Ensure = 'Present' + InstanceName = $Node.InstanceName + ServerName = $Node.NodeName + PsDscRunAsCredential = $SqlAdministratorCredential + } + if ( $Node.Role -eq 'PrimaryReplica' ) { # Create the availability group on the instance tagged as the primary replica @@ -71,7 +81,7 @@ Configuration Example Name = 'TestAG' InstanceName = $Node.InstanceName ServerName = $Node.NodeName - DependsOn = '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' + DependsOn = '[SqlAlwaysOnService]EnableHADR', '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' PsDscRunAsCredential = $SqlAdministratorCredential } } diff --git a/Examples/Resources/SqlAG/3-CreateAvailabilityGroupDetailed.ps1 b/Examples/Resources/SqlAG/3-CreateAvailabilityGroupDetailed.ps1 index a04d163ee..1b026e1b9 100644 --- a/Examples/Resources/SqlAG/3-CreateAvailabilityGroupDetailed.ps1 +++ b/Examples/Resources/SqlAG/3-CreateAvailabilityGroupDetailed.ps1 @@ -45,7 +45,8 @@ Configuration Example Import-DscResource -ModuleName SqlServerDsc - Node $AllNodes.NodeName { + Node $AllNodes.NodeName + { # Adding the required service account to allow the cluster to log into SQL SqlServerLogin AddNTServiceClusSvc { @@ -80,6 +81,14 @@ Configuration Example PsDscRunAsCredential = $SqlAdministratorCredential } + SqlAlwaysOnService EnableHADR + { + Ensure = 'Present' + InstanceName = $Node.InstanceName + ServerName = $Node.NodeName + PsDscRunAsCredential = $SqlAdministratorCredential + } + if ( $Node.Role -eq 'PrimaryReplica' ) { # Create the availability group on the instance tagged as the primary replica @@ -104,7 +113,7 @@ Configuration Example DatabaseHealthTrigger = $Node.DatabaseHealthTrigger DtcSupportEnabled = $Node.DtcSupportEnabled - DependsOn = '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' + DependsOn = '[SqlAlwaysOnService]EnableHADR', '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' PsDscRunAsCredential = $SqlAdministratorCredential } } diff --git a/Examples/Resources/SqlAGDatabase/1-AddDatabaseToAvailabilityGroup.ps1 b/Examples/Resources/SqlAGDatabase/1-AddDatabaseToAvailabilityGroup.ps1 index 014b729a0..c48e2c70d 100644 --- a/Examples/Resources/SqlAGDatabase/1-AddDatabaseToAvailabilityGroup.ps1 +++ b/Examples/Resources/SqlAGDatabase/1-AddDatabaseToAvailabilityGroup.ps1 @@ -12,7 +12,7 @@ $ConfigurationData = @{ AllNodes = @( @{ NodeName = '*' - SQLInstanceName = 'MSSQLSERVER' + InstanceName = 'MSSQLSERVER' AvailabilityGroupName = 'TestAG' }, @@ -38,7 +38,8 @@ Configuration Example Import-DscResource -ModuleName SqlServerDsc - Node $AllNodes.NodeName { + Node $AllNodes.NodeName + { # Adding the required service account to allow the cluster to log into SQL SqlServerLogin AddNTServiceClusSvc { @@ -46,7 +47,7 @@ Configuration Example Name = 'NT SERVICE\ClusSvc' LoginType = 'WindowsUser' ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName PsDscRunAsCredential = $SqlAdministratorCredential } @@ -56,7 +57,7 @@ Configuration Example DependsOn = '[SqlServerLogin]AddNTServiceClusSvc' Ensure = 'Present' ServerName = $Node.NodeName - InstanceName = $Node.SqlInstanceName + InstanceName = $Node.InstanceName Principal = 'NT SERVICE\ClusSvc' Permission = 'AlterAnyAvailabilityGroup', 'ViewServerState' PsDscRunAsCredential = $SqlAdministratorCredential @@ -69,7 +70,15 @@ Configuration Example Ensure = 'Present' Port = 5022 ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName + PsDscRunAsCredential = $SqlAdministratorCredential + } + + SqlAlwaysOnService EnableHADR + { + Ensure = 'Present' + InstanceName = $Node.InstanceName + ServerName = $Node.NodeName PsDscRunAsCredential = $SqlAdministratorCredential } @@ -80,9 +89,9 @@ Configuration Example { Ensure = 'Present' Name = $Node.AvailabilityGroupName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName ServerName = $Node.NodeName - DependsOn = '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' + DependsOn = '[SqlAlwaysOnService]EnableHADR', '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' PsDscRunAsCredential = $SqlAdministratorCredential } } @@ -96,9 +105,10 @@ Configuration Example Name = $Node.NodeName AvailabilityGroupName = $Node.AvailabilityGroupName ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName PrimaryReplicaServerName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).NodeName - PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).SQLInstanceName + PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).InstanceName + DependsOn = '[SqlAlwaysOnService]EnableHADR' } } @@ -109,7 +119,7 @@ Configuration Example AvailabilityGroupName = $Node.AvailabilityGroupName BackupPath = '\\SQL1\AgInitialize' DatabaseName = 'DB*', 'AdventureWorks' - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName ServerName = $Node.NodeName Ensure = 'Present' ProcessOnlyOnActiveNode = $true diff --git a/Examples/Resources/SqlAGDatabase/3-MatchDefinedDatabaseInAvailabilityGroup.ps1 b/Examples/Resources/SqlAGDatabase/3-MatchDefinedDatabaseInAvailabilityGroup.ps1 index bf09492bb..e8218117b 100644 --- a/Examples/Resources/SqlAGDatabase/3-MatchDefinedDatabaseInAvailabilityGroup.ps1 +++ b/Examples/Resources/SqlAGDatabase/3-MatchDefinedDatabaseInAvailabilityGroup.ps1 @@ -7,7 +7,7 @@ $ConfigurationData = @{ AllNodes = @( @{ NodeName = '*' - SQLInstanceName = 'MSSQLSERVER' + InstanceName = 'MSSQLSERVER' AvailabilityGroupName = 'TestAG' }, @@ -33,7 +33,8 @@ Configuration Example Import-DscResource -ModuleName SqlServerDsc - Node $AllNodes.NodeName { + Node $AllNodes.NodeName + { # Adding the required service account to allow the cluster to log into SQL SqlServerLogin AddNTServiceClusSvc { @@ -41,7 +42,7 @@ Configuration Example Name = 'NT SERVICE\ClusSvc' LoginType = 'WindowsUser' ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName PsDscRunAsCredential = $SqlAdministratorCredential } @@ -51,7 +52,7 @@ Configuration Example DependsOn = '[SqlServerLogin]AddNTServiceClusSvc' Ensure = 'Present' ServerName = $Node.NodeName - InstanceName = $Node.SqlInstanceName + InstanceName = $Node.InstanceName Principal = 'NT SERVICE\ClusSvc' Permission = 'AlterAnyAvailabilityGroup', 'ViewServerState' PsDscRunAsCredential = $SqlAdministratorCredential @@ -64,7 +65,15 @@ Configuration Example Ensure = 'Present' Port = 5022 ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName + PsDscRunAsCredential = $SqlAdministratorCredential + } + + SqlAlwaysOnService EnableHADR + { + Ensure = 'Present' + InstanceName = $Node.InstanceName + ServerName = $Node.NodeName PsDscRunAsCredential = $SqlAdministratorCredential } @@ -75,9 +84,9 @@ Configuration Example { Ensure = 'Present' Name = $Node.AvailabilityGroupName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName ServerName = $Node.NodeName - DependsOn = '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' + DependsOn = '[SqlAlwaysOnService]EnableHADR', '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' PsDscRunAsCredential = $SqlAdministratorCredential } } @@ -91,9 +100,10 @@ Configuration Example Name = $Node.NodeName AvailabilityGroupName = $Node.AvailabilityGroupName ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName PrimaryReplicaServerName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).NodeName - PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).SQLInstanceName + PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).InstanceName + DependsOn = '[SqlAlwaysOnService]EnableHADR' } } @@ -104,7 +114,7 @@ Configuration Example AvailabilityGroupName = $Node.AvailabilityGroupName BackupPath = '\\SQL1\AgInitialize' DatabaseName = 'DB*', 'AdventureWorks' - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName ServerName = $Node.NodeName Ensure = 'Present' Force = $true diff --git a/Examples/Resources/SqlAGReplica/1-CreateAvailabilityGroupReplica.ps1 b/Examples/Resources/SqlAGReplica/1-CreateAvailabilityGroupReplica.ps1 index e4e82e2f1..08fac49ce 100644 --- a/Examples/Resources/SqlAGReplica/1-CreateAvailabilityGroupReplica.ps1 +++ b/Examples/Resources/SqlAGReplica/1-CreateAvailabilityGroupReplica.ps1 @@ -12,7 +12,7 @@ $ConfigurationData = @{ AllNodes = @( @{ NodeName = '*' - SQLInstanceName = 'MSSQLSERVER' + InstanceName = 'MSSQLSERVER' AvailabilityGroupName = 'TestAG' ProcessOnlyOnActiveNode = $true }, @@ -39,7 +39,8 @@ Configuration Example Import-DscResource -ModuleName SqlServerDsc - Node $AllNodes.NodeName { + Node $AllNodes.NodeName + { # Adding the required service account to allow the cluster to log into SQL SqlServerLogin AddNTServiceClusSvc { @@ -47,7 +48,7 @@ Configuration Example Name = 'NT SERVICE\ClusSvc' LoginType = 'WindowsUser' ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName PsDscRunAsCredential = $SqlAdministratorCredential } @@ -57,7 +58,7 @@ Configuration Example DependsOn = '[SqlServerLogin]AddNTServiceClusSvc' Ensure = 'Present' ServerName = $Node.NodeName - InstanceName = $Node.SqlInstanceName + InstanceName = $Node.InstanceName Principal = 'NT SERVICE\ClusSvc' Permission = 'AlterAnyAvailabilityGroup', 'ViewServerState' PsDscRunAsCredential = $SqlAdministratorCredential @@ -70,7 +71,15 @@ Configuration Example Ensure = 'Present' Port = 5022 ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName + PsDscRunAsCredential = $SqlAdministratorCredential + } + + SqlAlwaysOnService EnableHADR + { + Ensure = 'Present' + InstanceName = $Node.InstanceName + ServerName = $Node.NodeName PsDscRunAsCredential = $SqlAdministratorCredential } @@ -81,9 +90,9 @@ Configuration Example { Ensure = 'Present' Name = $Node.AvailabilityGroupName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName ServerName = $Node.NodeName - DependsOn = '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' + DependsOn = '[SqlAlwaysOnService]EnableHADR', '[SqlServerEndpoint]HADREndpoint', '[SqlServerPermission]AddNTServiceClusSvcPermissions' PsDscRunAsCredential = $SqlAdministratorCredential } } @@ -97,9 +106,10 @@ Configuration Example Name = $Node.NodeName AvailabilityGroupName = $Node.AvailabilityGroupName ServerName = $Node.NodeName - InstanceName = $Node.SQLInstanceName + InstanceName = $Node.InstanceName PrimaryReplicaServerName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).NodeName - PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).SQLInstanceName + PrimaryReplicaInstanceName = ( $AllNodes | Where-Object { $_.Role -eq 'PrimaryReplica' } ).InstanceName + DependsOn = '[SqlAlwaysOnService]EnableHADR' ProcessOnlyOnActiveNode = $Node.ProcessOnlyOnActiveNode } } diff --git a/Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1 b/Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1 new file mode 100644 index 000000000..f4c9048ca --- /dev/null +++ b/Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1 @@ -0,0 +1,19 @@ +<# +.EXAMPLE + This example performs a standard Sql encryption setup. Forcing all connections to be encrypted. +#> +Configuration Example +{ + Import-DscResource -ModuleName SqlServerDsc + + node localhost { + SqlServerSecureConnection ForceSecureConnection + { + InstanceName = 'MSSQLSERVER' + Thumbprint = 'fb0b82c94b80da26cf0b86f10ec0c50ae7864a2c' + ForceEncryption = $true + Ensure = 'Present' + ServiceAccount = 'SqlSvc' + } + } +} diff --git a/Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1 b/Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1 new file mode 100644 index 000000000..0e544b0cb --- /dev/null +++ b/Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1 @@ -0,0 +1,19 @@ +<# +.EXAMPLE + This example performs a standard Sql encryption setup. Forcing all connections to be encrypted. +#> +Configuration Example +{ + Import-DscResource -ModuleName SqlServerDsc + + node localhost { + SqlServerSecureConnection SecureConnectionNotForced + { + InstanceName = 'MSSQLSERVER' + Thumbprint = 'fb0b82c94b80da26cf0b86f10ec0c50ae7864a2c' + ForceEncryption = $false + Ensure = 'Present' + ServiceAccount = 'SqlSvc' + } + } +} diff --git a/Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1 b/Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1 new file mode 100644 index 000000000..acf810862 --- /dev/null +++ b/Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1 @@ -0,0 +1,18 @@ +<# +.EXAMPLE + This example performs a standard Sql encryption setup. Forcing all connections to be encrypted. +#> +Configuration Example +{ + Import-DscResource -ModuleName SqlServerDsc + + node localhost { + SqlServerSecureConnection SecureConnectionAbsent + { + InstanceName = 'MSSQLSERVER' + Thumbprint = '' + Ensure = 'Absent' + ServiceAccount = 'SqlSvc' + } + } +} diff --git a/README.md b/README.md index d7e6cf55f..82f102776 100644 --- a/README.md +++ b/README.md @@ -47,16 +47,11 @@ by sending in pull requests yourself. please first check out the [Review Pull Request guidelines](https://github.com/PowerShell/DscResources/blob/master/CONTRIBUTING.md#reviewing-pull-requests), and the browse the list of [pull requests](https://github.com/PowerShell/SqlServerDsc/pulls) and look for those pull requests with label 'needs review'. -* If you want to improve the resources or tests, - or create a new resource, +* If you want to improve this resource module, then please check out the following guidelines. - * The [Contributing to the DSC Resource Kit](https://github.com/PowerShell/DscResources/blob/master/CONTRIBUTING.md) - guidelines. * The specific [Contributing to SqlServerDsc](https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md) guidelines. - * The common [Style Guidelines](https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md). - * The common [Best Practices](https://github.com/PowerShell/DscResources/blob/master/BestPractices.md) - guidelines. + * The common [Style Guidelines & Best Practices](https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md). * The common [Testing Guidelines](https://github.com/PowerShell/DscResources/blob/master/TestsGuidelines.md). * If you are new to GitHub (and git), then please check out [Getting Started with GitHub](https://github.com/PowerShell/DscResources/blob/master/GettingStartedWithGitHub.md). @@ -69,16 +64,18 @@ We are here for each other. ## Installation -To manually install the module, -download the source code and unzip the contents -of the '\Modules\SqlServerDsc' directory to the -'$env:ProgramFiles\WindowsPowerShell\Modules' folder. +### From GitHub source code + +To manually install the module, download the source code from GitHub and unzip +the contents to the '$env:ProgramFiles\WindowsPowerShell\Modules' folder. + +### From PowerShell Gallery To install from the PowerShell gallery using PowerShellGet (in PowerShell 5.0) run the following command: ```powershell -Find-Module -Name SqlServerDsc -Repository PSGallery | Install-Module +Find-Module -Name SqlServerDsc | Install-Module ``` To confirm installation, run the below command and ensure you see the SQL Server @@ -129,7 +126,7 @@ A full list of changes in each version can be found in the [change log](CHANGELO to manage database recovery model. * [**SqlDatabaseRole**](#sqldatabaserole) resource to manage SQL database roles. -* [**SqlRS**](#sqlrs) configures SQL Server Reporting +* [**SqlRS**](#sqlrs) configures SQL Server Reporting. Services to use a database engine in another instance. * [**SqlScript**](#sqlscript) resource to extend DSC Get/Set/Test functionality to T-SQL. @@ -155,6 +152,8 @@ A full list of changes in each version can be found in the [change log](CHANGELO * [**SqlServerReplication**](#sqlserverreplication) resource to manage SQL Replication distribution and publishing. * [**SqlServerRole**](#sqlserverrole) resource to manage SQL server roles. +* [**SqlServerSecureConnection**](#sqlserversecureconnection) resource to + enable encrypted SQL connections. * [**SqlServiceAccount**](#sqlserviceaccount) Manage the service account for SQL Server services. * [**SqlSetup**](#sqlsetup) installs a standalone SQL Server instance. @@ -492,7 +491,7 @@ Enables or disabled SQL Server Always On high availability and disaster recovery #### Examples * [Enable SQL Server Always On](/Examples/Resources/SqlAlwaysOnService/1-EnableAlwaysOn.ps1) -* [Disable SQL Server Always On](/Examples/Resources/SqlAlwaysOnService/1-DisableAlwaysOn.ps1) +* [Disable SQL Server Always On](/Examples/Resources/SqlAlwaysOnService/2-DisableAlwaysOn.ps1) #### Known issues @@ -982,6 +981,7 @@ Resource to manage SQL Server Database Mail. * **`[String]` Ensure** _(Write)_: Specifies the desired state of the Database Mail. When set to 'Present', the Database Mail will be created. When set to 'Absent', the Database Mail will be removed. Default value is 'Present'. + { *Present* | Absent }. * **`[String]` ProfileName** _(Required)_: The name of the Database Mail profile. * **`[String]` Description** _(Write)_: The description for the Database Mail profile and account. @@ -1290,7 +1290,7 @@ SQL Max Memory = TotalPhysicalMemory - (NumOfSQLThreads\*ThreadStackSize) - * [Set SQLServerMaxMemory to 12GB](/Examples/Resources/SqlServerMemory/1-SetMaxMemoryTo12GB.ps1) * [Set SQLServerMaxMemory to Auto](/Examples/Resources/SqlServerMemory/2-SetMaxMemoryToAuto.ps1) * [Set SQLServerMinMemory to 2GB and SQLServerMaxMemory to Auto](/Examples/Resources/SqlServerMemory/3-SetMinMemoryToFixedValueAndMaxMemoryToAuto.ps1) -* [Set SQLServerMaxMemory to Default](/Examples/Resources/SqlServerMemory/3-SetMaxMemoryToDefault.ps1) +* [Set SQLServerMaxMemory to Default](/Examples/Resources/SqlServerMemory/4-SetMaxMemoryToDefault.ps1) #### Known issues @@ -1461,6 +1461,48 @@ server roles, please read the below articles. All issues are not listed here, see [here for all open issues](https://github.com/PowerShell/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlServerRole). +### SqlServerSecureConnection + +Configures SQL connections to be encrypted. +Read more about encrypted connections in this article [Enable Encrypted Connections](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine). + +#### Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* You must have a Certificate that is trusted and issued for + `ServerAuthentication`. +* The name of the Certificate must be the fully qualified domain name (FQDN) + of the computer. +* The Certificate must be installed in the LocalMachine Personal store. +* If `PsDscRunAsCredential` common parameter is used to run the resource, the + specified credential must have permissions to connect to the SQL Server instance + specified in `InstanceName`. + +#### Parameters + +* **`[String]` InstanceName** _(Key)_: Name of the SQL Server instance to be + configured. +* **`[String]` Thumbprint** _(Required)_: Thumbprint of the certificate being + used for encryption. If parameter Ensure is set to 'Absent', then the + parameter Certificate can be set to an empty string. +* **`[String]` ServiceAccount** _(Required)_: Name of the account running the + SQL Server service. +* **`[String]` Ensure** _(Write)_: If Encryption should be Enabled (Present) + or Disabled (Absent). { *Present* | Absent }. Defaults to Present. +* **`[Boolean]` ForceEncryption** _(Write)_: If all connections to the SQL + instance should be encrypted. If this parameter is not assigned a value, + the default is, set to *True*, that all connections must be encrypted. + +#### Examples + +* [Force Secure Connection](Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1). +* [Secure Connection but not required](Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1). +* [Secure Connection disabled](Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1). + +#### Known issues + +All issues are not listed here, see [here for all open issues](https://github.com/PowerShell/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlServerSecureConnection). + ### SqlServiceAccount Manage the service account for SQL Server services. diff --git a/SqlServerDsc.psd1 b/SqlServerDsc.psd1 index 1fa94ae13..4503a033e 100644 --- a/SqlServerDsc.psd1 +++ b/SqlServerDsc.psd1 @@ -1,6 +1,6 @@ @{ # Version number of this module. - moduleVersion = '12.0.0.0' + moduleVersion = '12.1.0.0' # ID used to uniquely identify this module GUID = '693ee082-ed36-45a7-b490-88b07c86b42f' @@ -49,26 +49,58 @@ # IconUri = '' # ReleaseNotes of this module - ReleaseNotes = '- Changes to SqlServerDatabaseMail - - DisplayName is now properly treated as display name - for the originating email address ([issue 1200](https://github.com/PowerShell/SqlServerDsc/issue/1200)). - [Nick Reilingh (@NReilingh)](https://github.com/NReilingh) - - DisplayName property now defaults to email address instead of server name. - - Minor improvements to documentation. -- Changes to SqlAGDatabase - - Corrected reference to "PsDscRunAsAccount" in documentation - ([issue 1199](https://github.com/PowerShell/SqlServerDsc/issues/1199)). - [Nick Reilingh (@NReilingh)](https://github.com/NReilingh) -- Changes to SqlDatabaseOwner - - BREAKING CHANGE: Support multiple instances on the same node. - The parameter InstanceName is now Key and cannot be omitted - ([issue 1197](https://github.com/PowerShell/SqlServerDsc/issues/1197)). -- Changes to SqlSetup - - Added new parameters to allow to define the startup types for the Sql Engine - service, the Agent service, the Analysis service and the Integration Service. - The new optional parameters are respectively SqlSvcStartupType, AgtSvcStartupType, - AsSvcStartupType, IsSvcStartupType and RsSvcStartupType ([issue 1165](https://github.com/PowerShell/SqlServerDsc/issues/1165). - [Maxime Daniou (@mdaniou)](https://github.com/mdaniou) + ReleaseNotes = '- Changes to SqlServerDsc + - Add support for validating the code with the DSC ResourceKit + Script Analyzer rules, both in Visual Studio Code and directly using + `Invoke-ScriptAnalyzer`. + - Opt-in for common test "Common Tests - Validate Markdown Links". + - Updated broken links in `\README.md` and in `\Examples\README.md` + - Opt-in for common test "Common Tests - Relative Path Length". + - Updated the Installation section in the README.md. + - Updated the Contributing section in the README.md after + [Style Guideline and Best Practices guidelines](https://github.com/PowerShell/DscResources/blob/master/StyleGuidelines.md) + has merged into one document. + - To speed up testing in AppVeyor, unit tests are now run in two containers. + - Adding the PowerShell script `Assert-TestEnvironment.ps1` which + must be run prior to running any unit tests locally with + `Invoke-Pester`. + Read more in the specific contributing guidelines, under the section + [Unit Tests](https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.mdunit-tests). +- Changes to SqlServerDscHelper + - Fix style guideline lint errors. + - Changes to Connect-SQL + - Adding verbose message in Connect-SQL so it + now shows the username that is connecting. + - Changes to Import-SQLPS + - Fixed so that when importing SQLPS it imports + using the path (and not the .psd1 file). + - Fixed so that the verbose message correctly + shows the name, version and path when importing + the module SQLPS (it did show correctly for the + SqlServer module). +- Changes to SqlAg, SqlAGDatabase, and SqlAGReplica examples + - Included configuration for SqlAlwaysOnService to enable + HADR on each node to avoid confusion + ([issue 1182](https://github.com/PowerShell/SqlServerDsc/issues/1182)). +- Changes to SqlServerDatabaseMail + - Minor update to Ensure parameter description in the README.md. +- Changes to Write-ModuleStubFile.ps1 + - Create aliases for cmdlets in the stubbed module which have aliases + ([issue 1224](https://github.com/PowerShell/SqlServerDsc/issues/1224)). + [Dan Reist (@randomnote1)](https://github.com/randomnote1) + - Use a string builder to build the function stubs. + - Fixed formatting issues for the function to work with modules other + than SqlServer. +- New DSC resource SqlServerSecureConnection + - New resource to configure a SQL Server instance for encrypted SQL + connections. +- Changes to SqlAlwaysOnService + - Updated integration tests to use NetworkingDsc + ([issue 1129](https://github.com/PowerShell/SqlServerDsc/issues/1129)). +- Changes to SqlServiceAccount + - Fix unit tests that didn"t mock some of the calls. It no longer fail + when a SQL Server installation is not present on the node running the + unit test ([issue 983](https://github.com/PowerShell/SqlServerDsc/issues/983)). ' @@ -93,3 +125,4 @@ + diff --git a/SqlServerDscHelper.psm1 b/SqlServerDscHelper.psm1 index 8fc106edb..00222aeb6 100644 --- a/SqlServerDscHelper.psm1 +++ b/SqlServerDscHelper.psm1 @@ -29,14 +29,17 @@ function Connect-SQL [CmdletBinding()] param ( + [Parameter()] [ValidateNotNull()] [System.String] $SQLServer = $env:COMPUTERNAME, + [Parameter()] [ValidateNotNull()] [System.String] $SQLInstanceName = 'MSSQLSERVER', + [Parameter()] [ValidateNotNull()] [System.Management.Automation.PSCredential] $SetupCredential, @@ -64,6 +67,8 @@ function Connect-SQL if ($LoginType -eq 'SqlLogin') { + $connectUsername = $SetupCredential.Username + $sql.ConnectionContext.LoginSecure = $false $sql.ConnectionContext.Login = $SetupCredential.Username $sql.ConnectionContext.SecurePassword = $SetupCredential.Password @@ -71,11 +76,18 @@ function Connect-SQL if ($LoginType -eq 'WindowsUser') { + $connectUsername = $SetupCredential.GetNetworkCredential().UserName + $sql.ConnectionContext.ConnectAsUser = $true $sql.ConnectionContext.ConnectAsUserPassword = $SetupCredential.GetNetworkCredential().Password $sql.ConnectionContext.ConnectAsUserName = $SetupCredential.GetNetworkCredential().UserName } + Write-Verbose -Message ( + 'Connecting using the credential ''{0}'' and the login type ''{1}''.' ` + -f $connectUsername, $LoginType + ) -Verbose + $sql.ConnectionContext.ServerInstance = $databaseEngineInstance $sql.ConnectionContext.Connect() } @@ -252,19 +264,19 @@ function New-TerminatingError [System.String] $ErrorType, - [Parameter(Mandatory = $false)] + [Parameter()] [System.String[]] $FormatArgs, - [Parameter(Mandatory = $false)] + [Parameter()] [System.Management.Automation.ErrorCategory] $ErrorCategory = [System.Management.Automation.ErrorCategory]::OperationStopped, - [Parameter(Mandatory = $false)] + [Parameter()] [System.Object] $TargetObject = $null, - [Parameter(Mandatory = $false)] + [Parameter()] [System.Exception] $InnerException = $null ) @@ -645,7 +657,7 @@ function Import-SQLPSModule if ($availableModule) { # This sets $availableModuleName to the Path of the module to be loaded. - $availableModuleName = $availableModule.Path + $availableModuleName = Split-Path -Path $availableModule.Path -Parent } } @@ -660,9 +672,19 @@ function Import-SQLPSModule SQLPS has unapproved verbs, disable checking to ignore Warnings. Suppressing verbose so all cmdlet is not listed. #> - Import-Module -Name $availableModuleName -DisableNameChecking -Verbose:$False -Force:$Force -ErrorAction Stop + $importedModule = Import-Module -Name $availableModuleName -DisableNameChecking -Verbose:$false -Force:$Force -PassThru -ErrorAction Stop + + <# + SQLPS returns two entries, one with module type 'Script' and another with module type 'Manifest'. + Only return the object with module type 'Manifest'. + SqlServer only returns one object (of module type 'Script'), so no need to do anything for SqlServer module. + #> + if ($availableModuleName -ne 'SqlServer') + { + $importedModule = $importedModule | Where-Object -Property 'ModuleType' -EQ -Value 'Manifest' + } - Write-Verbose -Message ($script:localizedData.ImportedPowerShellModule -f $availableModule.Name, $availableModule.Version, $availableModule.Path) -Verbose + Write-Verbose -Message ($script:localizedData.ImportedPowerShellModule -f $importedModule.Name, $importedModule.Version, $importedModule.Path) -Verbose } catch { diff --git a/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 b/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 index 9a5b513cd..6661cfb5d 100644 --- a/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 +++ b/Tests/Integration/MSFT_SqlAlwaysOnService.config.ps1 @@ -44,7 +44,7 @@ $ConfigurationData = @{ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config { Import-DscResource -ModuleName 'PSDscResources' - Import-DscResource -ModuleName 'xNetworking' + Import-DscResource -ModuleName 'NetworkingDsc' node localhost { @@ -60,7 +60,7 @@ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config Name = 'RSAT-Clustering-PowerShell' } - xIPAddress LoopbackAdapterIPv4Address + IPAddress LoopbackAdapterIPv4Address { IPAddress = $Node.LoopbackAdapterIpAddress InterfaceAlias = $Node.LoopbackAdapterName @@ -72,7 +72,7 @@ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config loopback adapter as clustered network. This will be removed directly after the cluster has been created. #> - xDefaultGatewayAddress LoopbackAdapterIPv4DefaultGateway + DefaultGatewayAddress LoopbackAdapterIPv4DefaultGateway { Address = $Node.LoopbackAdapterGateway InterfaceAlias = $Node.LoopbackAdapterName @@ -166,14 +166,14 @@ Configuration MSFT_SqlAlwaysOnService_CreateDependencies_Config Configuration MSFT_SqlAlwaysOnService_CleanupDependencies_Config { - Import-DscResource -ModuleName 'xNetworking' + Import-DscResource -ModuleName 'NetworkingDsc' node localhost { <# Removing the default gateway from the loopback adapter. #> - xDefaultGatewayAddress LoopbackAdapterIPv4DefaultGateway + DefaultGatewayAddress LoopbackAdapterIPv4DefaultGateway { InterfaceAlias = $Node.LoopbackAdapterName AddressFamily = 'IPv4' diff --git a/Tests/Integration/MSFT_SqlServerSecureConnection.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerSecureConnection.Integration.Tests.ps1 new file mode 100644 index 000000000..7d39536ce --- /dev/null +++ b/Tests/Integration/MSFT_SqlServerSecureConnection.Integration.Tests.ps1 @@ -0,0 +1,154 @@ +<# + This is used to make sure the integration test run in the correct order. + The integration test should run after the integration tests SqlServerLogin + and SqlServerRole, so any problems in those will be caught first, since + these integration tests are using those resources. +#> +[Microsoft.DscResourceKit.IntegrationTest(OrderNumber = 5)] +param() + +$script:DSCModuleName = 'SqlServerDsc' +$script:DSCResourceFriendlyName = 'SqlServerSecureConnection' +$script:DSCResourceName = "MSFT_$($script:DSCResourceFriendlyName)" + +if (-not $env:APPVEYOR -eq $true) +{ + Write-Warning -Message ('Integration test for {0} will be skipped unless $env:APPVEYOR equals $true' -f $script:DSCResourceName) + return +} + +#region HEADER +# Integration Test Template Version: 1.1.2 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Integration + +$testRootFolderPath = Split-Path -Path $PSScriptRoot -Parent +Import-Module -Name (Join-Path -Path $testRootFolderPath -ChildPath (Join-Path -Path 'TestHelpers' -ChildPath 'CommonTestHelper.psm1')) -Force + +#endregion + +$mockSqlServicePrimaryAccountUserName = "$env:COMPUTERNAME\svc-SqlPrimary" + +$null = New-SQLSelfSignedCertificate +$mockSqlPrivateKeyPassword = ConvertTo-SecureString -String '1234' -AsPlainText -Force +Import-PfxCertificate -FilePath $env:SqlPrivateCertificatePath -Password $mockSqlPrivateKeyPassword -Exportable -CertStoreLocation 'Cert:\LocalMachine\Root' +Import-PfxCertificate -FilePath $env:SqlPrivateCertificatePath -Password $mockSqlPrivateKeyPassword -Exportable -CertStoreLocation 'Cert:\LocalMachine\My' +try +{ + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName).config.ps1" + . $configFile + + Describe "$($script:DSCResourceName)_Integration" { + BeforeAll { + $resourceId = "[$($script:DSCResourceFriendlyName)]Integration_Test" + } + + $configurationName = "$($script:DSCResourceName)_AddSecureConnection_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + SqlServicePrimaryUserName = $mockSqlServicePrimaryAccountUserName + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName + } | Where-Object -FilterScript { + $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Thumbprint | Should -Be $env:SqlCertificateThumbprint + $resourceCurrentState.ForceEncryption | Should -Be $true + } + } + + $configurationName = "$($script:DSCResourceName)_RemoveSecureConnection_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + SqlServicePrimaryUserName = $mockSqlServicePrimaryAccountUserName + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName + } | Where-Object -FilterScript { + $_.ResourceId -eq $resourceId + } + + $resultObject.Thumbprint | Should -BeNullOrEmpty + $resourceCurrentState.ForceEncryption | Should -Be $false + } + } + } +} +finally +{ + #region FOOTER + + Restore-TestEnvironment -TestEnvironment $TestEnvironment + + #endregion +} diff --git a/Tests/Integration/MSFT_SqlServerSecureConnection.config.ps1 b/Tests/Integration/MSFT_SqlServerSecureConnection.config.ps1 new file mode 100644 index 000000000..b69c9d317 --- /dev/null +++ b/Tests/Integration/MSFT_SqlServerSecureConnection.config.ps1 @@ -0,0 +1,62 @@ +$ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + ServerName = $env:COMPUTERNAME + InstanceName = 'DSCSQL2016' + + Thumbprint = $env:SqlCertificateThumbprint + + CertificateFile = $env:DscPublicCertificatePath + } + ) +} + +Configuration MSFT_SqlServerSecureConnection_AddSecureConnection_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $SqlServicePrimaryUserName + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost + { + SqlServerSecureConnection 'Integration_Test' + { + InstanceName = $Node.InstanceName + Ensure = 'Present' + Thumbprint = $Node.Thumbprint + ServiceAccount = $SqlServicePrimaryUserName + ForceEncryption = $true + } + } +} + +Configuration MSFT_SqlServerSecureConnection_RemoveSecureConnection_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $SqlServicePrimaryUserName + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost + { + SqlServerSecureConnection 'Integration_Test' + { + InstanceName = $Node.InstanceName + Ensure = 'Absent' + Thumbprint = '' + ServiceAccount = $SqlServicePrimaryUserName + } + } +} diff --git a/Tests/SqlServerDscCommon.Tests.ps1 b/Tests/SqlServerDscCommon.Tests.ps1 deleted file mode 100644 index 9fb391035..000000000 --- a/Tests/SqlServerDscCommon.Tests.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -$script:moduleRoot = Split-Path $PSScriptRoot -Parent - -Describe 'SqlServerDsc module common tests' { - Context -Name 'When the resource should be used to compile a configuration in Azure Automation' { - $fullPathHardLimit = 129 # 129 characters is the current maximum for a relative path to be able to compile configurations in Azure Automation. - $allModuleFiles = Get-ChildItem -Path $script:moduleRoot -Recurse - - $testCaseModuleFile = @() - $allModuleFiles | ForEach-Object -Process { - $testCaseModuleFile += @( - @{ - FullRelativePath = $_.FullName -replace ($script:moduleRoot -replace '\\','\\') - } - ) - } - - It 'The length of the full path '''' should not exceed the max hard limit' -TestCases $testCaseModuleFile { - param - ( - [Parameter()] - [System.String] - $FullRelativePath - ) - - $FullRelativePath.Length | Should -Not -BeGreaterThan $fullPathHardLimit - } - } -} diff --git a/Tests/TestHelpers/CommonTestHelper.psm1 b/Tests/TestHelpers/CommonTestHelper.psm1 index fdcce7fa9..ad6115b3d 100644 --- a/Tests/TestHelpers/CommonTestHelper.psm1 +++ b/Tests/TestHelpers/CommonTestHelper.psm1 @@ -228,3 +228,61 @@ function Get-NetIPAddressNetwork return $networkObject } + + + +<# + .SYNOPSIS + This command will create a new self-signed certificate to be used to + secure Sql Server connection. + + .OUTPUTS + Returns the created certificate. Writes the path to the public + certificate in the machine environment variable $env:sqlPrivateCertificatePath, + and the certificate thumbprint in the machine environment variable + $env:SqlCertificateThumbprint. +#> +function New-SQLSelfSignedCertificate +{ + $sqlPublicCertificatePath = Join-Path -Path $env:temp -ChildPath 'SqlPublicKey.cer' + $sqlPrivateCertificatePath = Join-Path -Path $env:temp -ChildPath 'SqlPrivateKey.cer' + $sqlPriavteKeyPassword = ConvertTo-SecureString -String "1234" -Force -AsPlainText + + $certificateSubject = $env:COMPUTERNAME + + <# + There are build workers still on Windows Server 2012 R2 so let's + use the alternate method of New-SelfSignedCertificate. + #> + Install-Module -Name PSPKI -Scope CurrentUser -Force + Import-Module -Name PSPKI + + $newSelfSignedCertificateExParameters = @{ + Subject = "CN=$certificateSubject" + EKU = 'Server Authentication' + KeyUsage = 'KeyEncipherment, DataEncipherment' + SAN = "dns:$certificateSubject" + FriendlyName = 'Sql Encryption certificate' + Path = $sqlPrivateCertificatePath + Password = $sqlPriavteKeyPassword + Exportable = $true + KeyLength = 2048 + ProviderName = 'Microsoft Enhanced Cryptographic Provider v1.0' + AlgorithmName = 'RSA' + SignatureAlgorithm = 'SHA256' + } + + $certificate = New-SelfSignedCertificateEx @newSelfSignedCertificateExParameters + + Write-Info -Message ('Created self-signed certificate ''{0}'' with thumbprint ''{1}''.' -f $certificate.Subject, $certificate.Thumbprint) + + # Update a machine and session environment variable with the path to the private certificate. + Set-EnvironmentVariable -Name 'SqlPrivateCertificatePath' -Value $sqlPrivateCertificatePath -Machine + Write-Info -Message ('Environment variable $env:SqlPrivateCertificatePath set to ''{0}''' -f $env:SqlPrivateCertificatePath) + + # Update a machine and session environment variable with the thumbprint of the certificate. + Set-EnvironmentVariable -Name 'SqlCertificateThumbprint' -Value $certificate.Thumbprint -Machine + Write-Info -Message ('Environment variable $env:SqlCertificateThumbprint set to ''{0}''' -f $env:SqlCertificateThumbprint) + + return $certificate +} diff --git a/Tests/Tests.depend.psd1 b/Tests/Tests.depend.psd1 new file mode 100644 index 000000000..3446741b8 --- /dev/null +++ b/Tests/Tests.depend.psd1 @@ -0,0 +1,42 @@ +<# + .DESCRIPTION + This is the dependency file for use with Assert-TestEnvironment.ps1 and/or + Invoke-PSDepend (PSSDepend). +#> +@{ + RemoveTestFramework = @{ + DependencyType = 'Command' + Source = ' + $testFrameWorkPath = Join-Path -Path $PWD -ChildPath ''DscResource.Tests'' + if (Test-Path -Path $testFrameWorkPath) + { + Write-Verbose -Message ''Removing local test framework repository.'' + Remove-Item -Path (Join-Path -Path $PWD -ChildPath ''DscResource.Tests'') -Recurse -Force + } + ' + } + + 'CloneTestFramework' = @{ + DependencyType = 'Git' + Name = 'https://github.com/PowerShell/DscResource.Tests' + Version = 'dev' + DependsOn = 'RemoveTestFramework' + } + + LoadDscResourceKitTypes = @{ + DependencyType = 'Command' + Source = ' + if (-not (''Microsoft.DscResourceKit.Test'' -as [Type])) + { + Write-Verbose -Message ''Loading the Microsoft.DscResourceKit types into the current session.'' + $typesSourceFile = Join-Path -Path ''$PWD\DscResource.Tests'' -ChildPath ''Microsoft.DscResourceKit.cs'' + Add-Type -Path $typesSourceFile -WarningAction SilentlyContinue + } + else + { + Write-Verbose -Message ''The Microsoft.DscResourceKit types was already loaded into the current session.'' + } + ' + DependsOn = 'CloneTestFramework' + } +} diff --git a/Tests/Unit/CommonResourceHelper.Tests.ps1 b/Tests/Unit/CommonResourceHelper.Tests.ps1 index b2a75ed00..6a69d0ffe 100644 --- a/Tests/Unit/CommonResourceHelper.Tests.ps1 +++ b/Tests/Unit/CommonResourceHelper.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for helper functions in module CommonResourceHelper. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + Describe 'CommonResourceHelper Unit Tests' { BeforeAll { # Import the CommonResourceHelper module to test diff --git a/Tests/Unit/MSFT_SqlAG.Tests.ps1 b/Tests/Unit/MSFT_SqlAG.Tests.ps1 index 75b305637..92ee1e2df 100644 --- a/Tests/Unit/MSFT_SqlAG.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAG.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlAG DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + #region HEADER # Unit Test Template Version: 1.2.1 diff --git a/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 b/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 index a71ea6255..9786856db 100644 --- a/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlAGDatabase DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + #region HEADER # Unit Test Template Version: 1.2.0 diff --git a/Tests/Unit/MSFT_SqlAGListener.Tests.ps1 b/Tests/Unit/MSFT_SqlAGListener.Tests.ps1 index 01c2feaef..5150c71c0 100644 --- a/Tests/Unit/MSFT_SqlAGListener.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAGListener.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlAGListener DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlAGListener' diff --git a/Tests/Unit/MSFT_SqlAGReplica.Tests.ps1 b/Tests/Unit/MSFT_SqlAGReplica.Tests.ps1 index 62d5a339f..c3eca28c9 100644 --- a/Tests/Unit/MSFT_SqlAGReplica.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAGReplica.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlAGReplica DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + #region HEADER # Unit Test Template Version: 1.2.0 diff --git a/Tests/Unit/MSFT_SqlAlias.Tests.ps1 b/Tests/Unit/MSFT_SqlAlias.Tests.ps1 index eee3a1096..a02551ec0 100644 --- a/Tests/Unit/MSFT_SqlAlias.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAlias.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlAlias DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + #region HEADER # Unit Test Template Version: 1.2.1 diff --git a/Tests/Unit/MSFT_SqlAlwaysOnService.Tests.ps1 b/Tests/Unit/MSFT_SqlAlwaysOnService.Tests.ps1 index a488cc991..29dd82fee 100644 --- a/Tests/Unit/MSFT_SqlAlwaysOnService.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAlwaysOnService.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlAlwaysOnService DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlAlwaysOnService' diff --git a/Tests/Unit/MSFT_SqlDatabase.Tests.ps1 b/Tests/Unit/MSFT_SqlDatabase.Tests.ps1 index feefe742a..b14acc183 100644 --- a/Tests/Unit/MSFT_SqlDatabase.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlDatabase.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlDatabase DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlDatabase' diff --git a/Tests/Unit/MSFT_SqlDatabaseDefaultLocation.Tests.ps1 b/Tests/Unit/MSFT_SqlDatabaseDefaultLocation.Tests.ps1 index cd3867fbe..0dc41e661 100644 --- a/Tests/Unit/MSFT_SqlDatabaseDefaultLocation.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlDatabaseDefaultLocation.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlDatabaseDefaultLocation DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + #region Header # Unit Test Template Version: 1.2.1 diff --git a/Tests/Unit/MSFT_SqlDatabaseOwner.Tests.ps1 b/Tests/Unit/MSFT_SqlDatabaseOwner.Tests.ps1 index 9364456b1..961c259ef 100644 --- a/Tests/Unit/MSFT_SqlDatabaseOwner.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlDatabaseOwner.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlDatabaseOwner DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlDatabaseOwner' diff --git a/Tests/Unit/MSFT_SqlDatabasePermission.Tests.ps1 b/Tests/Unit/MSFT_SqlDatabasePermission.Tests.ps1 index d5d36ce5c..01dd6672d 100644 --- a/Tests/Unit/MSFT_SqlDatabasePermission.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlDatabasePermission.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlDatabasePermission DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlDatabasePermission' diff --git a/Tests/Unit/MSFT_SqlDatabaseRecoveryModel.Tests.ps1 b/Tests/Unit/MSFT_SqlDatabaseRecoveryModel.Tests.ps1 index 62e2bb414..d1a8ee4fc 100644 --- a/Tests/Unit/MSFT_SqlDatabaseRecoveryModel.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlDatabaseRecoveryModel.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlDatabaseRecoveryModel DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlDatabaseRecoveryModel' diff --git a/Tests/Unit/MSFT_SqlDatabaseRole.Tests.ps1 b/Tests/Unit/MSFT_SqlDatabaseRole.Tests.ps1 index d8d4f6a75..4eb18da38 100644 --- a/Tests/Unit/MSFT_SqlDatabaseRole.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlDatabaseRole.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlDatabaseRole DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlDatabaseRole' diff --git a/Tests/Unit/MSFT_SqlRS.Tests.ps1 b/Tests/Unit/MSFT_SqlRS.Tests.ps1 index 04d8a73df..abc35f536 100644 --- a/Tests/Unit/MSFT_SqlRS.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlRS.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlRS DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlRS' diff --git a/Tests/Unit/MSFT_SqlScript.Tests.ps1 b/Tests/Unit/MSFT_SqlScript.Tests.ps1 index c8154bc89..9e746e75f 100644 --- a/Tests/Unit/MSFT_SqlScript.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlScript.Tests.ps1 @@ -1,11 +1,18 @@ <# .SYNOPSIS - Automated unit test for MSFT_SqlScript DSC Resource + Automated unit test for MSFT_SqlScript DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment #> +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] # Suppression of this PSSA rule allowed in tests. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] -Param() +param() #region HEADER diff --git a/Tests/Unit/MSFT_SqlScriptQuery.Tests.ps1 b/Tests/Unit/MSFT_SqlScriptQuery.Tests.ps1 index df9b71de7..75ec443c0 100644 --- a/Tests/Unit/MSFT_SqlScriptQuery.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlScriptQuery.Tests.ps1 @@ -1,8 +1,15 @@ <# .SYNOPSIS - Automated unit test for MSFT_SqlScriptQuery DSC Resource + Automated unit test for MSFT_SqlScriptQuery DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment #> +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] # Suppression of this PSSA rule allowed in tests. [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] Param() diff --git a/Tests/Unit/MSFT_SqlServerConfiguration.Tests.ps1 b/Tests/Unit/MSFT_SqlServerConfiguration.Tests.ps1 index 9ec3236e4..7f51c0b74 100644 --- a/Tests/Unit/MSFT_SqlServerConfiguration.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerConfiguration.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerConfiguration DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerConfiguration' diff --git a/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 index adaee05d4..03adf6c3b 100644 --- a/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerDatabaseMail.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerDatabaseMail DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerDatabaseMail' diff --git a/Tests/Unit/MSFT_SqlServerEndpoint.Tests.ps1 b/Tests/Unit/MSFT_SqlServerEndpoint.Tests.ps1 index 907bde075..7c1350292 100644 --- a/Tests/Unit/MSFT_SqlServerEndpoint.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerEndpoint.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerEndpoint DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerEndpoint' diff --git a/Tests/Unit/MSFT_SqlServerEndpointPermission.Tests.ps1 b/Tests/Unit/MSFT_SqlServerEndpointPermission.Tests.ps1 index 67d8c9295..9c866b203 100644 --- a/Tests/Unit/MSFT_SqlServerEndpointPermission.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerEndpointPermission.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerEndpointPermission DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerEndpointPermission' diff --git a/Tests/Unit/MSFT_SqlServerEndpointState.Tests.ps1 b/Tests/Unit/MSFT_SqlServerEndpointState.Tests.ps1 index f34634bd9..b5bba8c29 100644 --- a/Tests/Unit/MSFT_SqlServerEndpointState.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerEndpointState.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerEndpointState DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerEndpointState' diff --git a/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 b/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 index bc883b96c..c1da064de 100644 --- a/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 @@ -1,3 +1,15 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerLogin DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] # Suppressing this rule because PlainText is required for one of the functions used in this test [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] param() diff --git a/Tests/Unit/MSFT_SqlServerMaxDop.Tests.ps1 b/Tests/Unit/MSFT_SqlServerMaxDop.Tests.ps1 index a2cedfa39..eeaac7bda 100644 --- a/Tests/Unit/MSFT_SqlServerMaxDop.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerMaxDop.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerMaxDop DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerMaxDop' diff --git a/Tests/Unit/MSFT_SqlServerMemory.Tests.ps1 b/Tests/Unit/MSFT_SqlServerMemory.Tests.ps1 index 9dd19dcc7..cebaee00b 100644 --- a/Tests/Unit/MSFT_SqlServerMemory.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerMemory.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerMemory DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerMemory' diff --git a/Tests/Unit/MSFT_SqlServerNetwork.Tests.ps1 b/Tests/Unit/MSFT_SqlServerNetwork.Tests.ps1 index 65f459662..1f39d5bf2 100644 --- a/Tests/Unit/MSFT_SqlServerNetwork.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerNetwork.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerNetwork DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerNetwork' diff --git a/Tests/Unit/MSFT_SqlServerPermission.Tests.ps1 b/Tests/Unit/MSFT_SqlServerPermission.Tests.ps1 index f85dec57d..e17ef628d 100644 --- a/Tests/Unit/MSFT_SqlServerPermission.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerPermission.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerPermission DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerPermission' diff --git a/Tests/Unit/MSFT_SqlServerReplication.Tests.ps1 b/Tests/Unit/MSFT_SqlServerReplication.Tests.ps1 index 863113136..742259445 100644 --- a/Tests/Unit/MSFT_SqlServerReplication.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerReplication.Tests.ps1 @@ -1,5 +1,18 @@ +<# + .N<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerReplication DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "")] -param () +param() $script:DSCModuleName = 'SqlServerDSC' $script:DSCResourceName = 'MSFT_SqlServerReplication' diff --git a/Tests/Unit/MSFT_SqlServerRole.Tests.ps1 b/Tests/Unit/MSFT_SqlServerRole.Tests.ps1 index f2cc26954..7fece5089 100644 --- a/Tests/Unit/MSFT_SqlServerRole.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerRole.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerRole DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServerRole' diff --git a/Tests/Unit/MSFT_SqlServerSecureConnection.Tests.ps1 b/Tests/Unit/MSFT_SqlServerSecureConnection.Tests.ps1 new file mode 100644 index 000000000..6e190a498 --- /dev/null +++ b/Tests/Unit/MSFT_SqlServerSecureConnection.Tests.ps1 @@ -0,0 +1,763 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServerSecureConnection DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + +$script:DSCModuleName = 'SqlServerDsc' +$script:DSCResourceName = 'MSFT_SqlServerSecureConnection' + +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup +{ + Import-Module -Name (Join-Path -Path (Join-Path -Path (Join-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'Tests') -ChildPath 'Unit') -ChildPath 'Stubs') -ChildPath 'SQLPSStub.psm1') -Global -Force +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + class MockedAccessControl + { + [hashtable]$Access = @{ + IdentityReference = 'Everyone' + FileSystemRights = @{ + value__ = '131209' + } + } + + [void] AddAccessRule([System.Security.AccessControl.FileSystemAccessRule] $object) + { + + } + } + + class MockedGetItem + { + [string] $Thumbprint = '12345678' + [string] $PSPath = 'PathToItem' + [string] $Path = 'PathToItem' + [MockedAccessControl]$ACL = [MockedAccessControl]::new() + [hashtable]$PrivateKey = @{ + CspKeyContainerInfo = @{ + UniqueKeyContainerName = 'key' + } + } + + [MockedGetItem] GetAccessControl() + { + return $this + } + } + + $mockNamedInstanceName = 'INSTANCE' + $mockDefaultInstanceName = 'MSSQLSERVER' + $mockThumbprint = '123456789' + $mockServiceAccount = 'SqlSvc' + + Describe 'SqlServerSecureConnection\Get-TargetResource' -Tag 'Get' { + BeforeAll { + $mockDynamic_SqlBuildVersion = '13.0.4001.0' + + $defaultParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + Context 'When the system is in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + + } + + Context 'When the system is not in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $false + Certificate = '987654321' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It "Should return the state of absent when certificate thumbprint and certificate permissions don't match." { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Not -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $false + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = '987654321' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the state of absent when certificate permissions match but encryption settings dont' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Not -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It 'Should return the state of absent when certificate permissions dont match but encryption settings do' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + $defaultParameters.Ensure = 'Absent' + + Context 'When the system is in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $false + Certificate = '' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of absent' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -BeNullOrEmpty + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $false + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $false + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present when ForceEncryption is False but a thumbprint exist.' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $false + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = '' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present when Certificate is null but ForceEncryption is True.' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -BeNullOrEmpty + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + } + + Describe 'SqlServerSecureConnection\Set-TargetResource' -Tag 'Set' { + BeforeAll { + $defaultParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + + $defaultAbsentParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + + Mock -CommandName Set-EncryptedConnectionSetting -Verifiable + Mock -CommandName Set-CertificatePermission -Verifiable + Mock -CommandName Restart-SqlService -Verifiable + } + + Context 'When the system is not in the desired state' { + + Context 'When only certificate permissions are set' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = '987654321' + ServiceAccount = $mockServiceAccount + ForceEncryption = $false + Ensure = 'Present' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should configure only ForceEncryption and Certificate thumbprint' { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 0 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + + Context 'When there is no certificate permissions set' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + It 'Should configure only certificate permissions' { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 0 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + + Context 'When no settings are configured' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = '987654321' + ServiceAccount = $mockServiceAccount + ForceEncryption = $false + Ensure = 'Present' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It 'Should configure Encryption settings and certificate permissions' { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + + Context 'When ensure is absent' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It 'Should configure Encryption settings setting certificate to empty string' { + { Set-TargetResource @defaultAbsentParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 0 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + } + } + + Describe 'SqlServerSecureConnection\Test-TargetResource' -Tag 'Test' { + Context 'When the system is not in the desired state' { + Context 'When ForceEncryption is not configured properly' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $false + Ensure = 'Absent' + } + } -Verifiable + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + + Context 'When Thumbprint is not configured properly' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = '987654321' + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } -Verifiable + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + + Context 'When certificate permission is not set' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } -Verifiable + + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + + Context 'When Ensure is Absent' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } -Verifiable + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } -Verifiable + + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $true + } + } + } + + Describe 'SqlServerSecureConnection\Get-EncryptedConnectionSetting' -Tag 'Helper' { + + Mock -CommandName 'Get-ItemProperty' -MockWith { + return @{ + ForceEncryption = '1' + } + } -ParameterFilter { $Name -eq 'ForceEncryption' } -Verifiable + Mock -CommandName 'Get-ItemProperty' -MockWith { + return @{ + Certificate = '12345678' + } + } -ParameterFilter { $Name -eq 'Certificate' } -Verifiable + + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should return hashtable with ForceEncryption and Certificate' { + $result = Get-EncryptedConnectionSetting -InstanceName 'NamedInstance' + $result.Certificate | Should -Be '12345678' + $result.ForceEncryption | Should -Be 1 + $result | Should -BeOfType [Hashtable] + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return $null + } + } + + It 'Should return null' { + $result = Get-EncryptedConnectionSetting -InstanceName 'NamedInstance' + $result | Should -BeNullOrEmpty + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Set-EncryptedConnectionSetting' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return [MockedGetItem]::new() + } + Mock -CommandName 'Set-ItemProperty' -Verifiable + } + + It 'Should not throw' { + { Set-EncryptedConnectionSetting -InstanceName 'NamedInstance' -Thumbprint '12345678' -ForceEncryption $true } | Should -Not -Throw + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return $null + } -Verifiable + Mock -CommandName 'Set-ItemProperty' + } + + It 'Should throw' { + { Set-EncryptedConnectionSetting -InstanceName 'NamedInstance' -Thumbprint '12345678' -ForceEncryption $true } | Should -Throw + Assert-MockCalled -CommandName 'Set-ItemProperty' -Times 0 + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Test-CertificatePermission' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should return True' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $true + Assert-VerifiableMock + } + } + + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + $mockedItem = [MockedGetItem]::new() + $mockedItem.ACL = $null + return $mockedItem + } -Verifiable + } + + It 'Should return False when no permissions were found' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $False + Assert-VerifiableMock + } + } + + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + $mockedItem = [MockedGetItem]::new() + $mockedItem.ACL.FileSystemRights.Value__ = 1 + return $mockedItem + } -Verifiable + } + + It 'Should return False when the wrong permissions are added' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $False + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + return $null + } -Verifiable + } + + It 'Should return False' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $false + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Set-CertificatePermission' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + Mock -CommandName 'Set-Acl' -Verifiable + } + + It 'Should not throw' { + { Set-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' } | Should -Not -Throw + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-Item' -MockWith { + return $null + } -Verifiable + Mock -CommandName 'Get-ChildItem' -MockWith { + return $null + } -Verifiable + } + + It 'Should throw' { + { Set-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' } | Should -Throw + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Get-CertificateAcl' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-ChildItem' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + + Mock -CommandName 'Get-Item' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should not throw' { + { Get-CertificateAcl -Thumbprint '12345678' } | Should -Not -Throw + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Get-SqlEncryptionValue' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-ItemProperty' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + Mock -CommandName 'Get-Item' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should not throw' { + { Get-SqlEncryptionValue -InstanceName $mockNamedInstanceName } | Should -Not -Throw + Assert-VerifiableMock + } + } + + Context 'When calling a method that execute unsuccessfully' { + BeforeAll { + Mock -CommandName 'Get-ItemProperty' -MockWith { + throw "Error" + } -Verifiable + Mock -CommandName 'Get-Item' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should throw with expected message' { + { Get-SqlEncryptionValue -InstanceName $mockNamedInstanceName } | Should -Throw -ExpectedMessage "SQL instance '$mockNamedInstanceName' not found on SQL Server." + Assert-VerifiableMock + } + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 b/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 index 033323d5b..1db06c2e6 100644 --- a/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlServiceAccount DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + $script:DSCModuleName = 'SqlServerDsc' $script:DSCResourceName = 'MSFT_SqlServiceAccount' @@ -120,45 +134,50 @@ try return $managedComputerObject } - # Creates a new ManagedComputer object for a default instance with a local service account - $mockNewObject_ManagedComputer_DefaultInstance_LocalServiceAccount = { + $mockGetServiceObject_DefaultInstance = { $managedComputerObject = New-Object -TypeName PSObject -Property @{ - Name = $mockSqlServer - Services = @( - New-Object -TypeName PSObject -Property @{ Name = $mockDefaultInstanceName - ServiceAccount = ($mockLocalServiceAccountName -replace $mockSqlServer, '.') + ServiceAccount = $mockDefaultServiceAccountName Type = 'SqlServer' } - ) - } - $managedComputerObject.Services | ForEach-Object { - $_ | Add-Member @mockAddMemberParameters_SetServiceAccount - } + $managedComputerObject | Add-Member @mockAddMemberParameters_SetServiceAccount return $managedComputerObject } - <# - Creates a new ManagedComputer object for a default instance that throws an exception - when attempting to set the service account - #> - $mockNewObject_ManagedComputer_DefaultInstance_SetServiceAccountException = { + $mockGetServiceObject_DefaultInstance_SetServiceAccount_ThrowException = { $managedComputerObject = New-Object -TypeName PSObject -Property @{ - Name = $mockSqlServer - Services = @( - New-Object -TypeName PSObject -Property @{ Name = $mockDefaultInstanceName ServiceAccount = $mockDefaultServiceAccountName Type = 'SqlServer' } - ) - } - $managedComputerObject.Services | ForEach-Object { - $_ | Add-Member @mockAddMemberParameters_SetServiceAccount_Exception - } + $managedComputerObject | Add-Member @mockAddMemberParameters_SetServiceAccount_Exception + + return $managedComputerObject + } + + $mockGetServiceObject_DefaultInstance_LocalServiceAccount = { + $managedComputerObject = New-Object -TypeName PSObject -Property @{ + Name = $mockDefaultInstanceName + ServiceAccount = ($mockLocalServiceAccountName -replace $mockSqlServer, '.') + Type = 'SqlServer' + } + + $managedComputerObject | Add-Member @mockAddMemberParameters_SetServiceAccount + + return $managedComputerObject + } + + $mockGetServiceObject_NamedInstance = { + $managedComputerObject = New-Object -TypeName PSObject -Property @{ + Name = ('MSSQL${0}' -f $mockNamedInstance) + ServiceAccount = $mockDesiredServiceAccountName + Type = 'SqlServer' + } + + $managedComputerObject | Add-Member @mockAddMemberParameters_SetServiceAccount return $managedComputerObject } @@ -567,7 +586,9 @@ try } Describe 'MSFT_SqlServerServiceAccount\Get-ServiceObject' -Tag 'Helper' { - Mock -CommandName Import-SQLPSModule -Verifiable + BeforeAll { + Mock -CommandName Import-SQLPSModule -Verifiable + } $defaultGetServiceObjectParameters = @{ ServerName = $mockSqlServer @@ -577,6 +598,9 @@ try Context 'When getting the service information for a default instance' { Mock @mockNewObjectParameters_DefaultInstance + Mock -CommandName Get-SqlServiceName -MockWith { + return 'MSSQLServer' + } It 'Should have the correct Type for the service' { $getServiceObjectParameters = $defaultGetServiceObjectParameters.Clone() @@ -593,6 +617,9 @@ try Context 'When getting the service information for a named instance' { Mock @mockNewObjectParameters_NamedInstance + Mock -CommandName Get-SqlServiceName -MockWith { + return ('MSSQL${0}' -f $mockNamedInstance) + } It 'Should have the correct Type for the service' { $getServiceObjectParameters = $defaultGetServiceObjectParameters.Clone() @@ -609,10 +636,18 @@ try } Describe 'MSFT_SqlServerServiceAccount\Get-TargetResource' -Tag 'Get' { - Mock -CommandName Import-SQLPSModule -Verifiable - Context 'When getting the service information for a default instance' { - Mock @mockNewObjectParameters_DefaultInstance + BeforeAll { + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_DefaultInstance -ParameterFilter { + $ServiceType -eq 'DatabaseEngine' + } + + Mock -CommandName Get-ServiceObject -MockWith { + return $null + } -ParameterFilter { + $ServiceType -eq 'SQLServerAgent' + } + } $defaultGetTargetResourceParameters = @{ ServerName = $mockSqlServer @@ -632,8 +667,7 @@ try $testServiceInformation.ServiceAccountName | Should -Be $mockDefaultServiceAccountName # Ensure mocks were properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } It 'Should throw the correct exception when an invalid ServiceType and InstanceName are specified' { @@ -644,13 +678,22 @@ try Should -Throw "The SQLServerAgent service on $($mockSqlServer)\$($mockDefaultInstanceName) could not be found." # Ensure mocks were properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } } Context 'When getting the service information for a named instance' { - Mock @mockNewObjectParameters_NamedInstance + BeforeAll { + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_NamedInstance -ParameterFilter { + $ServiceType -eq 'DatabaseEngine' + } + + Mock -CommandName Get-ServiceObject -MockWith { + return $null + } -ParameterFilter { + $ServiceType -eq 'SQLServerAgent' + } + } # Splat the function parameters $defaultGetTargetResourceParameters = @{ @@ -671,8 +714,7 @@ try $testServiceInformation.ServiceAccountName | Should -Be $mockDesiredServiceAccountName # Ensure mocks were properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } It 'Should throw the correct exception when an invalid ServiceType and InstanceName are specified' { @@ -683,16 +725,14 @@ try Should -Throw "The SQLServerAgent service on $($mockSqlServer)\$($mockNamedInstance) could not be found." # Ensure mocks were properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } } Context 'When the service account is local to the machine' { - $mockNewObjectParameters = $mockNewObjectParameters_DefaultInstance.Clone() - $mockNewObjectParameters.MockWith = $mockNewObject_ManagedComputer_DefaultInstance_LocalServiceAccount - - Mock @mockNewObjectParameters + BeforeAll { + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_DefaultInstance_LocalServiceAccount + } $defaultGetTargetResourceParameters = @{ ServerName = $mockSqlServer @@ -708,16 +748,21 @@ try $currentState.ServiceAccountName | Should -Be $mockLocalServiceAccountName # Ensure mocks were properly used - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } } } Describe 'MSFT_SqlServerServiceAccount\Test-TargetResource' -Tag 'Test' { - Mock -CommandName Import-SQLPSModule -Verifiable - Context 'When the system is not in the desired state for a default instance' { - Mock @mockNewObjectParameters_DefaultInstance + Mock -CommandName Get-TargetResource -MockWith { + return @{ + ServerName = $ServerName + InstanceName = $mockDefaultInstanceName + ServiceType = 'SqlServer' + ServiceAccountName = 'NotExpectedAccount' + } + } It 'Should return false' { $testTargetResourceParameters = @{ @@ -730,13 +775,19 @@ try Test-TargetResource @testTargetResourceParameters | Should -Be $false # Ensure mocks are properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Exactly -Times 1 } } Context 'When the system is in the desired state or a default instance' { - Mock @mockNewObjectParameters_DefaultInstance + Mock -CommandName Get-TargetResource -MockWith { + return @{ + ServerName = $ServerName + InstanceName = $mockDefaultInstanceName + ServiceType = 'SqlServer' + ServiceAccountName = $mockDefaultServiceAccountName + } + } It 'Should return true' { $testTargetResourceParameters = @{ @@ -749,15 +800,12 @@ try Test-TargetResource @testTargetResourceParameters | Should -Be $true # Ensure mocks are properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Exactly -Times 1 } } Context 'When the system is in the desired state and Force is specified' { - Mock @mockNewObjectParameters_DefaultInstance - - It 'Should return False when Force is specified' { + It 'Should always return $false when Force is specified' { $testTargetResourceParameters = @{ ServerName = $mockSqlServer InstanceName = $mockDefaultInstanceName @@ -767,14 +815,18 @@ try } Test-TargetResource @testTargetResourceParameters | Should -Be $false - - # Ensure mocks are properly used - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 0 } } Context 'When the system is not in the desired state for a named instance' { - Mock @mockNewObjectParameters_NamedInstance + Mock -CommandName Get-TargetResource -MockWith { + return @{ + ServerName = $ServerName + InstanceName = $mockNamedInstance + ServiceType = 'SqlServer' + ServiceAccountName = 'NotExpectedAccount' + } + } It 'Should return false' { $testTargetResourceParameters = @{ @@ -787,14 +839,19 @@ try Test-TargetResource @testTargetResourceParameters | Should -Be $false # Ensure mocks are properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Exactly -Times 1 } } Context 'When the system is in the desired state for a named instance' { - Mock @mockNewObjectParameters_NamedInstance - + Mock -CommandName Get-TargetResource -MockWith { + return @{ + ServerName = $ServerName + InstanceName = $mockNamedInstance + ServiceType = 'SqlServer' + ServiceAccountName = $mockDesiredServiceAccountName + } + } It 'Should return true' { $testTargetResourceParameters = @{ ServerName = $mockSqlServer @@ -806,8 +863,7 @@ try Test-TargetResource @testTargetResourceParameters | Should -Be $true # Ensure mocks are properly used - Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Exactly -Times 1 } } @@ -833,8 +889,6 @@ try } Describe 'MSFT_SqlServerServiceAccount\Set-TargetResource' -Tag 'Set' { - Mock -CommandName Import-SQLPSModule -Verifiable - Context 'When changing the service account for the default instance' { BeforeAll { $defaultSetTargetResourceParameters = @{ @@ -844,7 +898,15 @@ try ServiceAccount = $mockDefaultServiceAccountCredential } - Mock @mockNewObjectParameters_DefaultInstance + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_DefaultInstance -ParameterFilter { + $ServiceType -eq 'DatabaseEngine' + } + + Mock -CommandName Get-ServiceObject -MockWith { + return $null + } -ParameterFilter { + $ServiceType -eq 'SQLServerAgent' + } Mock -CommandName Restart-SqlService -Verifiable } @@ -867,7 +929,7 @@ try $testServiceAccountUpdated.NewPassword | Should -Be $setTargetResourceParameters.ServiceAccount.GetNetworkCredential().Password # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 0 } @@ -882,7 +944,7 @@ try { Set-TargetResource @setTargetResourceParameters } | Should -Throw $mockCorrectErrorMessage # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 0 } @@ -895,31 +957,12 @@ try Set-TargetResource @setTargetResourceParameters # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 1 } - - It 'Should throw the correct exception if SetServiceAccount call fails' { - $newObjectParameters = $mockNewObjectParameters_DefaultInstance.Clone() - $newObjectParameters.MockWith = $mockNewObject_ManagedComputer_DefaultInstance_SetServiceAccountException - - Mock @newObjectParameters - - $setTargetResourceParameters = $defaultSetTargetResourceParameters.Clone() - - # Get the localized error message - $mockCorrectErrorMessage = $script:localizedData.SetServiceAccountFailed -f $setTargetResourceParameters.ServerName, $setTargetResourceParameters.InstanceName, '' - - # Attempt to update the service account - { Set-TargetResource @setTargetResourceParameters } | Should -Throw $mockCorrectErrorMessage - - # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 0 - } } - Context 'When changing the service account for the default instance' { + Context 'When changing the service account for the named instance' { BeforeAll { $defaultSetTargetResourceParameters = @{ ServerName = $mockSqlServer @@ -928,7 +971,15 @@ try ServiceAccount = $mockDefaultServiceAccountCredential } - Mock @mockNewObjectParameters_NamedInstance + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_DefaultInstance -ParameterFilter { + $ServiceType -eq 'DatabaseEngine' + } + + Mock -CommandName Get-ServiceObject -MockWith { + return $null + } -ParameterFilter { + $ServiceType -eq 'SQLServerAgent' + } Mock -CommandName Restart-SqlService -Verifiable } @@ -951,7 +1002,7 @@ try $testServiceAccountUpdated.NewPassword | Should -Be $setTargetResourceParameters.ServiceAccount.GetNetworkCredential().Password # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 0 } @@ -966,7 +1017,7 @@ try { Set-TargetResource @setTargetResourceParameters } | Should -Throw $mockCorrectErrorMessage # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 0 } @@ -979,27 +1030,27 @@ try Set-TargetResource @setTargetResourceParameters # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 1 } + } - It 'Should throw the correct exception if SetServiceAccount call fails' { - $newObjectParameters = $mockNewObjectParameters_NamedInstance.Clone() - $newObjectParameters.MockWith = $mockNewObject_ManagedComputer_NamedInstance_SetServiceAccountException - - Mock @newObjectParameters + Context 'When SetServiceAccount() method call fails' { + BeforeEach { + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_DefaultInstance_SetServiceAccount_ThrowException + } + It 'Should throw the correct exception' { $setTargetResourceParameters = $defaultSetTargetResourceParameters.Clone() - # Get the expected localized error message + # Get the localized error message $mockCorrectErrorMessage = $script:localizedData.SetServiceAccountFailed -f $setTargetResourceParameters.ServerName, $setTargetResourceParameters.InstanceName, '' - # Attempt to update the service information + # Attempt to update the service account { Set-TargetResource @setTargetResourceParameters } | Should -Throw $mockCorrectErrorMessage # Ensure mocks are used properly - Assert-MockCalled -CommandName New-Object -ParameterFilter $mockNewObject_ParameterFilter -Scope It -Exactly -Times 1 - Assert-MockCalled -CommandName Restart-SqlService -Scope It -Exactly -Times 0 + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } } } diff --git a/Tests/Unit/MSFT_SqlSetup.Tests.ps1 b/Tests/Unit/MSFT_SqlSetup.Tests.ps1 index f29cc5bff..8383d2051 100644 --- a/Tests/Unit/MSFT_SqlSetup.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlSetup.Tests.ps1 @@ -1,3 +1,15 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlSetup DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] # Suppressing this rule because PlainText is required for one of the functions used in this test [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] param() diff --git a/Tests/Unit/MSFT_SqlWaitForAG.Tests.ps1 b/Tests/Unit/MSFT_SqlWaitForAG.Tests.ps1 index 20fddd345..46f2f0e6a 100644 --- a/Tests/Unit/MSFT_SqlWaitForAG.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlWaitForAG.Tests.ps1 @@ -1,3 +1,20 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlWaitForAG DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + +$script:DSCModuleName = 'SqlServerDsc' +$script:DSCResourceName = 'MSFT_SqlWaitForAG' + #region HEADER # Unit Test Template Version: 1.2.1 diff --git a/Tests/Unit/MSFT_SqlWindowsFirewall.Tests.ps1 b/Tests/Unit/MSFT_SqlWindowsFirewall.Tests.ps1 index fff7115c9..dcf7c023d 100644 --- a/Tests/Unit/MSFT_SqlWindowsFirewall.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlWindowsFirewall.Tests.ps1 @@ -1,3 +1,17 @@ +<# + .SYNOPSIS + Automated unit test for MSFT_SqlWindowsFirewall DSC resource. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container2', ContainerImage = 'microsoft/windowsservercore')] +param() + #region HEADER # Unit Test Template Version: 1.2.1 diff --git a/Tests/Unit/SqlServerDSCHelper.Tests.ps1 b/Tests/Unit/SqlServerDSCHelper.Tests.ps1 index b83a7286c..371468004 100644 --- a/Tests/Unit/SqlServerDSCHelper.Tests.ps1 +++ b/Tests/Unit/SqlServerDSCHelper.Tests.ps1 @@ -1,3 +1,15 @@ +<# + .SYNOPSIS + Automated unit test for helper functions in module SqlServerDscHelper. + + .NOTES + To run this script locally, please make sure to first run the bootstrap + script. Read more at + https://github.com/PowerShell/SqlServerDsc/blob/dev/CONTRIBUTING.md#bootstrap-script-assert-testenvironment +#> + +# This is used to make sure the unit test run in a container. +[Microsoft.DscResourceKit.UnitTest(ContainerName = 'Container1', ContainerImage = 'microsoft/windowsservercore')] # To run these tests, we have to fake login credentials [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')] param () @@ -718,11 +730,59 @@ InModuleScope $script:moduleName { } } + <# + This is the path to the latest version of SQLPS, to test that only the + newest SQLPS module is returned. + #> + $sqlPsLatestModulePath = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' + + <# + For SQLPS module this should be the root of the module. + The .psd1 file is parsed from the module full path in the code. + #> + $sqlPsExpectedModulePath = Split-Path -Path $sqlPsLatestModulePath -Parent + + $mockImportModule = { if ($Name -ne $mockExpectedModuleNameToImport) { throw ('Wrong module was loaded. Expected {0}, but was {1}.' -f $mockExpectedModuleNameToImport, $Name[0]) } + + switch ($Name) + { + 'SqlServer' + { + $importModuleResult = @{ + ModuleType = 'Script' + Version = '21.0.17279' + Name = $Name + } + } + + $sqlPsExpectedModulePath + { + # Can not use $Name because that contain the path to the module manifest. + $importModuleResult = @( + @{ + ModuleType = 'Script' + Version = '0.0' + # Intentionally formatted to correctly mimic a real run. + Name = 'Sqlps' + Path = $sqlPsLatestModulePath + } + @{ + ModuleType = 'Manifest' + Version = '1.0' + # Intentionally formatted to correctly mimic a real run. + Name = 'sqlps' + Path = $sqlPsLatestModulePath + } + ) + } + } + + return $importModuleResult } $mockGetModuleSqlServer = { @@ -740,13 +800,12 @@ InModuleScope $script:moduleName { ) } - $sqlPsLatestModulePath = 'C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' - $mockGetModuleSqlPs = { # Return an array to test so that the latest version is only imported. return @( New-Object -TypeName PSObject -Property @{ Name = 'SQLPS' + # This is a path to an older version of SQL PS than $sqlPsLatestModulePath. Path = 'C:\Program Files (x86)\Microsoft SQL Server\120\Tools\PowerShell\Modules\SQLPS\Sqlps.ps1' } @@ -833,7 +892,7 @@ InModuleScope $script:moduleName { $PSBoundParameters.ContainsKey('Name') -eq $true } - $mockExpectedModuleNameToImport = $sqlPsLatestModulePath + $mockExpectedModuleNameToImport = $sqlPsExpectedModulePath } It 'Should import the SqlServer module without throwing' { @@ -854,7 +913,7 @@ InModuleScope $script:moduleName { } Context 'When neither SqlServer or SQLPS exists' { - $mockExpectedModuleNameToImport = $sqlPsLatestModulePath + $mockExpectedModuleNameToImport = $sqlPsExpectedModulePath It 'Should throw the correct error message' { Mock -CommandName Get-Module diff --git a/Tests/Unit/Stubs/Write-ModuleStubFile.ps1 b/Tests/Unit/Stubs/Write-ModuleStubFile.ps1 index f62c2d74c..b41ed803b 100644 --- a/Tests/Unit/Stubs/Write-ModuleStubFile.ps1 +++ b/Tests/Unit/Stubs/Write-ModuleStubFile.ps1 @@ -3,67 +3,185 @@ Generates a file contaning function stubs of all cmdlets from the module given as a parameter. .PARAMETER ModuleName - The name of the module to load and generate stubs from. This module must exist on the computer where this function is ran. + The name of the module to load and generate stubs from. This module must exist on the computer where this function is run. .PARAMETER Path - Path to where to write the stubs file. The filename will be generated from the module name. + Path to where to write the stubs file. The filename will be generated from the module name. The default path is the working directory. .EXAMPLE - Write-ModuleStubFile -ModuleName 'SqlServer' -Path 'C:\Source' + Write-ModuleStubFile -ModuleName OperationsManager + + .EXAMPLE + Write-ModuleStubFile -ModuleName SqlServer -Path C:\Source #> -function Write-ModuleStubFile { +function Write-ModuleStubFile +{ param ( - [Parameter( Mandatory )] - [System.String] $ModuleName, + [Parameter(Mandatory = $true)] + [System.String] + $ModuleName, - [Parameter( Mandatory )] - [System.String] $Path + [Parameter()] + [System.IO.DirectoryInfo] + $Path = ( Get-Location ).Path ) - Import-Module $ModuleName -DisableNameChecking -Force - - ( ( get-command -Module $ModuleName -CommandType 'Cmdlet' ) | ForEach-Object -Begin { - "# Suppressing this rule because these functions are from an external module" - "# and are only being used as stubs", - "[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingUserNameAndPassWordParams', '')]" - "param()" - "" - } -Process { - $signature = $null - $command = $_ + # Import the supplied module + Import-Module -Name $ModuleName -DisableNameChecking -Force -ErrorAction Stop + + # Get the module object + $module = Get-Module -Name $ModuleName + + # Define the output file name + $outFile = Join-Path -Path $Path -ChildPath "$($module.Name )_$($module.Version)_Stubs.psm1" + + # Verify the output file doesn't already exist + if ( Test-Path -Path $outFile ) + { + throw "The file '$outFile' already exists." + } + + # Define the length of the indent + $indent = ' ' * 4 + + # Define the header of the file + $headerStringBuilder = New-Object -TypeName System.Text.StringBuilder + $null = $headerStringBuilder.AppendLine('<#') + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.AppendLine('.SYNOPSIS') + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.AppendLine("Cmdlet stubs for the module $($module.Name).") + $null = $headerStringBuilder.AppendLine() + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.AppendLine('.DESCRIPTION') + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.AppendLine("This module contains the stubs for the cmdlets in the module $($module.Name) version $($module.Version.ToString()).") + $null = $headerStringBuilder.AppendLine() + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.AppendLine('.NOTES') + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.Append($indent) + $null = $headerStringBuilder.AppendLine("The stubs in this module were generated from the $($MyInvocation.MyCommand) function which is distributed as part of the SqlServerDsc module.") + $null = $headerStringBuilder.AppendLine('#>') + $null = $headerStringBuilder.AppendLine() + $null = $headerStringBuilder.AppendLine('# Suppressing this rule because these functions are from an external module and are only being used as stubs') + $null = $headerStringBuilder.AppendLine('[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute(''PSAvoidUsingUserNameAndPassWordParams'', '''')]') + $null = $headerStringBuilder.AppendLine('param()') + $headerStringBuilder.ToString() | Out-File -FilePath $outFile -Encoding utf8 -Append + + + # Get the cmdlets in the module + $cmdlets = Get-Command -Module $ModuleName -CommandType Cmdlet + + foreach ( $cmdlet in $cmdlets ) + { + # Clear the alias variable to ensure unnecessary aliases are not created + Remove-Variable -Name alias -ErrorAction SilentlyContinue + + # Create a string builder object to build the functions + $functionDefinition = New-Object -TypeName System.Text.StringBuilder + + # Reset the end of definition variable $endOfDefinition = $false - $metadata = New-Object -TypeName System.Management.Automation.CommandMetaData -ArgumentList $command + + # Get the Cmdlet metadata + $metadata = New-Object -TypeName System.Management.Automation.CommandMetaData -ArgumentList $cmdlet + + # Get the definition of the cmdlet $definition = [System.Management.Automation.ProxyCommand]::Create($metadata) - foreach ($line in $definition -split "`n") + + # Define the beginning of the function + $null = $functionDefinition.AppendLine("function $($cmdlet.Name)") + $null = $functionDefinition.AppendLine('{') + + + # Iterate over each line in the cmdlet + foreach ( $line in $definition.Split([System.Environment]::NewLine) ) { - $line = $line -replace '\[Microsoft.SqlServer.*.\]', '[object]' + # Reset variables which are used to determine what kind of line this is currently on + $endOfParameter = $false + $formatParam = $false + + # Make the objects generic to better support mocking + $line = $line -replace '\[Microsoft.[\d\w\.]+\[\]\]', '[System.Object[]]' + $line = $line -replace '\[Microsoft.[\d\w\.]+\]', '[System.Object]' $line = $line -replace 'SupportsShouldProcess=\$true, ', '' - if( $line.Contains( '})' ) ) + # Determine if any line modifications need to be made + switch -Regex ( $line.TrimEnd() ) + { + # Last line of param section + '\}\)$' + { + $line = $line -replace '\}\)(\s+)?$','}' + $endOfDefinition = $true + } + + # Last line of a parameter definition + ',$' + { + $endOfParameter = $true + } + + # Format Param line + 'param\($' + { + $line = $line -replace 'param\(','param' + $formatParam = $true + } + } + + # Write the current line with an indent + if ( -not [System.String]::IsNullOrEmpty($line.Trim()) ) { - $line = $line.Remove( $line.Length - 2 ) - $endOfDefinition = $true + $null = $functionDefinition.Append($indent) + $null = $functionDefinition.AppendLine($line.TrimEnd()) } - if( $line.Trim() -ne '' ) { - $signature += " $line" - } else { - $signature += $line + # Add a blank line after the parameter section + if ( $endOfParameter ) + { + $null = $functionDefinition.AppendLine() } - if( $endOfDefinition ) + # Move the right paranthesis at the end of the param section to a new line + if ( $endOfDefinition ) { - $signature += "`n )" + $null = $functionDefinition.Append($indent) + $null = $functionDefinition.AppendLine(')') break } + + # Move the left parenthesis to the next line after the "param" keyword + if ( $formatParam ) + { + $null = $functionDefinition.Append($indent) + $null = $functionDefinition.AppendLine('(') + } + } + + # Build the body of the function + $null = $functionDefinition.AppendLine() + $null = $functionDefinition.Append($indent) + $null = $functionDefinition.AppendLine('throw ''{0}: StubNotImplemented'' -f $MyInvocation.MyCommand') + $null = $functionDefinition.AppendLine('}') + $null = $functionDefinition.AppendLine() + + # Find any aliases which may exist for the cmdlet + $alias = Get-Alias -Definition $cmdlet.Name -ErrorAction SilentlyContinue + + # If any aliases exist + if ( $alias ) + { + # Create an alias in the stubs + $null = $functionDefinition.Append("New-Alias -Name $($alias.DisplayName) -Value $($alias.Definition)") + $null = $functionDefinition.AppendLine() } - "function $($command.Name) {" - "$signature" - "" - " throw '{0}: StubNotImplemented' -f $`MyInvocation.MyCommand" - "}" - "" - } ) | Out-String | Out-File ( Join-Path -Path $Path -ChildPath "$(( get-module $moduleName -ListAvailable).Name )Stub.psm1") -Encoding utf8 -Append + # Export the function text to the file + $functionDefinition.ToString() | Out-File -FilePath $outFile -Encoding utf8 -Append + } }