diff --git a/.vscode/settings.json b/.vscode/settings.json index 7c025cccb..774dce64a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -110,7 +110,8 @@ "checkpointing", "HRESULT", "RSDB", - "RSIP" + "RSIP", + "contoso" ], "cSpell.ignorePaths": [ ".git" diff --git a/CHANGELOG.md b/CHANGELOG.md index 86fedd4fa..5a9b4d209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -244,6 +244,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 by calling the `InitializeReportServer` CIM method. Used to complete initial configuration after database and URL setup ([issue #2014](https://github.com/dsccommunity/SqlServerDsc/issues/2014)). +- Added public commands `Set-SqlDscRSUnattendedExecutionAccount` and + `Remove-SqlDscRSUnattendedExecutionAccount` to manage the unattended execution + account for Reporting Services. These wrap the `SetUnattendedExecutionAccount` + CIM method. - Added public command `Get-SqlDscRSSslCertificate` to list available SSL certificates that can be used for Reporting Services. Wraps the `ListSSLCertificates` CIM method. @@ -338,6 +342,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Prerequisites Integration Tests - Added `svc-RS` local Windows user for Reporting Services service account integration testing. +- `Get-SqlDscRSConfiguration` + - Added retry logic with parameters `RetryCount`, `RetryDelaySeconds`, and + `SkipRetry` to handle intermittent CIM instance retrieval failures when + the Report Server service or WMI provider is not immediately ready. ### Fixed diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 82d1d8f14..3c429e710 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -594,6 +594,9 @@ stages: 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Post.ServiceAccountChange.SQL2017.RS.Integration.Tests.ps1' 'tests/Integration/Commands/Post.ServiceAccountChange.SQL2019-2022.RS.Integration.Tests.ps1' + # Group 7 + 'tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' # Group 8 'tests/Integration/Commands/Repair-SqlDscReportingService.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1' @@ -693,6 +696,9 @@ stages: 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Post.ServiceAccountChange.PowerBI.RS.Integration.Tests.ps1' + # Group 7 + 'tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' # Group 8 'tests/Integration/Commands/Repair-SqlDscPowerBIReportServer.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1' @@ -778,6 +784,9 @@ stages: 'tests/Integration/Commands/Get-SqlDscRSSslCertificateBinding.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSSslCertificateBinding.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSSslCertificateBinding.Integration.Tests.ps1' + # Group 7 + 'tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1' ) name: test displayName: 'Run Integration Test' @@ -1196,6 +1205,7 @@ stages: - Integration_Test_Commands_SqlServer_PreparedImage - Integration_Test_Commands_ReportingServices - Integration_Test_Commands_BIReportServer + - Integration_Test_Commands_BIReportServer_Secure - Integration_Test_Resources_SqlServer - Integration_Test_Resources_SqlServer_dbatools - Integration_Test_Resources_ReportingServices diff --git a/source/Public/Add-SqlDscRSSslCertificateBinding.ps1 b/source/Public/Add-SqlDscRSSslCertificateBinding.ps1 index df8119413..0449cad12 100644 --- a/source/Public/Add-SqlDscRSSslCertificateBinding.ps1 +++ b/source/Public/Add-SqlDscRSSslCertificateBinding.ps1 @@ -171,14 +171,13 @@ function Add-SqlDscRSSslCertificateBinding } catch { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.Add_SqlDscRSSslCertificateBinding_FailedToAdd -f $instanceName, $_.Exception.Message), - 'ASRSSCB0001', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $Configuration - ) - ) + $errorMessage = $script:localizedData.Add_SqlDscRSSslCertificateBinding_FailedToAdd -f $instanceName, $_.Exception.Message + + $exception = New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'ASRSSCB0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) } } diff --git a/source/Public/Get-SqlDscRSConfiguration.ps1 b/source/Public/Get-SqlDscRSConfiguration.ps1 index 997f1cd58..88fe52d97 100644 --- a/source/Public/Get-SqlDscRSConfiguration.ps1 +++ b/source/Public/Get-SqlDscRSConfiguration.ps1 @@ -16,6 +16,11 @@ 'WindowsServiceIdentityConfigured' and methods for managing Reporting Services configuration. + By default, if the CIM instance is not found on the first attempt, the + command will retry after a delay. This handles intermittent failures + when the Report Server service or WMI provider is not immediately ready. + Use `-SkipRetry` to disable retry behavior. + .PARAMETER InstanceName Specifies the name of the Reporting Services instance. This is a mandatory parameter. @@ -25,11 +30,23 @@ If not specified, the version is automatically detected using `Get-SqlDscRSSetupConfiguration`. + .PARAMETER RetryCount + Specifies the number of retry attempts if the CIM instance is not found. + Default is 1 retry attempt. + + .PARAMETER RetryDelaySeconds + Specifies the delay in seconds between retry attempts. Default is 30 + seconds. + + .PARAMETER SkipRetry + If specified, skips retry attempts and throws an error immediately if + the CIM instance is not found. + .EXAMPLE Get-SqlDscRSConfiguration -InstanceName 'SSRS' Returns the configuration CIM instance for the SSRS instance. The version - is automatically detected. + is automatically detected. Retries once after 30 seconds if not found. .EXAMPLE Get-SqlDscRSConfiguration -InstanceName 'SSRS' -Version 15 @@ -43,6 +60,18 @@ Gets the configuration CIM instance for the SSRS instance and enables secure connection using the pipeline. + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' -RetryCount 3 -RetryDelaySeconds 10 + + Returns the configuration CIM instance for the SSRS instance, retrying + up to 3 times with a 10-second delay between attempts if not found. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' -SkipRetry + + Returns the configuration CIM instance for the SSRS instance without + any retry attempts. Throws an error immediately if not found. + .INPUTS None. @@ -64,7 +93,21 @@ function Get-SqlDscRSConfiguration [Parameter()] [System.Int32] - $Version + $Version, + + [Parameter()] + [ValidateRange(1, [System.Int32]::MaxValue)] + [System.Int32] + $RetryCount = 1, + + [Parameter()] + [ValidateRange(1, [System.Int32]::MaxValue)] + [System.Int32] + $RetryDelaySeconds = 30, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SkipRetry ) if (-not $PSBoundParameters.ContainsKey('Version')) @@ -102,25 +145,54 @@ function Get-SqlDscRSConfiguration ErrorAction = 'Stop' } - try + $maxAttempts = if ($SkipRetry.IsPresent) { - $reportingServicesConfiguration = Get-CimInstance @getCimInstanceParameters + 1 } - catch + else { - $errorMessage = $script:localizedData.Get_SqlDscRSConfiguration_FailedToGetConfiguration -f $InstanceName, $_.Exception.Message + 1 + $RetryCount + } - $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru) -ErrorId 'GSRSCD0003' -ErrorCategory 'InvalidOperation' -TargetObject $InstanceName + $attempt = 0 + $reportingServicesConfiguration = $null - $PSCmdlet.ThrowTerminatingError($errorRecord) - } + while ($attempt -lt $maxAttempts) + { + $attempt++ + + try + { + $reportingServicesConfiguration = Get-CimInstance @getCimInstanceParameters + } + catch + { + $errorMessage = $script:localizedData.Get_SqlDscRSConfiguration_FailedToGetConfiguration -f $InstanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru) -ErrorId 'GSRSCD0003' -ErrorCategory 'InvalidOperation' -TargetObject $InstanceName + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + # Filter to ensure we get the correct instance if multiple are returned. + $reportingServicesConfiguration = $reportingServicesConfiguration | + Where-Object -FilterScript { + $_.InstanceName -eq $InstanceName + } - # Filter to ensure we get the correct instance if multiple are returned. - $reportingServicesConfiguration = $reportingServicesConfiguration | - Where-Object -FilterScript { - $_.InstanceName -eq $InstanceName + if ($reportingServicesConfiguration) + { + break } + if ($attempt -lt $maxAttempts) + { + Write-Debug -Message ($script:localizedData.Get_SqlDscRSConfiguration_RetryingAfterDelay -f $InstanceName, $attempt, $maxAttempts, $RetryDelaySeconds) + + Start-Sleep -Seconds $RetryDelaySeconds + } + } + if (-not $reportingServicesConfiguration) { $errorMessage = $script:localizedData.Get_SqlDscRSConfiguration_ConfigurationNotFound -f $InstanceName diff --git a/source/Public/Get-SqlDscRSSslCertificate.ps1 b/source/Public/Get-SqlDscRSSslCertificate.ps1 index 9eb43f51f..9df06bc2a 100644 --- a/source/Public/Get-SqlDscRSSslCertificate.ps1 +++ b/source/Public/Get-SqlDscRSSslCertificate.ps1 @@ -97,14 +97,13 @@ function Get-SqlDscRSSslCertificate } catch { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.Get_SqlDscRSSslCertificate_FailedToGet -f $instanceName, $_.Exception.Message), - 'GSRSSC0001', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $Configuration - ) - ) + $errorMessage = $script:localizedData.Get_SqlDscRSSslCertificate_FailedToGet -f $instanceName, $_.Exception.Message + + $exception = New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'GSRSSC0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) } } } diff --git a/source/Public/Get-SqlDscRSSslCertificateBinding.ps1 b/source/Public/Get-SqlDscRSSslCertificateBinding.ps1 index 9a91752d5..b9c16352a 100644 --- a/source/Public/Get-SqlDscRSSslCertificateBinding.ps1 +++ b/source/Public/Get-SqlDscRSSslCertificateBinding.ps1 @@ -115,14 +115,13 @@ function Get-SqlDscRSSslCertificateBinding } catch { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.Get_SqlDscRSSslCertificateBinding_FailedToGet -f $instanceName, $_.Exception.Message), - 'GSRSSCB0001', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $Configuration - ) - ) + $errorMessage = $script:localizedData.Get_SqlDscRSSslCertificateBinding_FailedToGet -f $instanceName, $_.Exception.Message + + $exception = New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'GSRSSCB0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) } } } diff --git a/source/Public/Remove-SqlDscRSSslCertificateBinding.ps1 b/source/Public/Remove-SqlDscRSSslCertificateBinding.ps1 index 6f3fe94d8..5c92903f0 100644 --- a/source/Public/Remove-SqlDscRSSslCertificateBinding.ps1 +++ b/source/Public/Remove-SqlDscRSSslCertificateBinding.ps1 @@ -92,7 +92,7 @@ function Remove-SqlDscRSSslCertificateBinding { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] @@ -169,14 +169,13 @@ function Remove-SqlDscRSSslCertificateBinding } catch { - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - ($script:localizedData.Remove_SqlDscRSSslCertificateBinding_FailedToRemove -f $instanceName, $_.Exception.Message), - 'RSRSSCB0001', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $Configuration - ) - ) + $errorMessage = $script:localizedData.Remove_SqlDscRSSslCertificateBinding_FailedToRemove -f $instanceName, $_.Exception.Message + + $exception = New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'RSRSSCB0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) } } diff --git a/source/Public/Remove-SqlDscRSUnattendedExecutionAccount.ps1 b/source/Public/Remove-SqlDscRSUnattendedExecutionAccount.ps1 new file mode 100644 index 000000000..076ad8df7 --- /dev/null +++ b/source/Public/Remove-SqlDscRSUnattendedExecutionAccount.ps1 @@ -0,0 +1,132 @@ +<# + .SYNOPSIS + Removes the unattended execution account from SQL Server Reporting Services. + + .DESCRIPTION + Removes the unattended execution account from SQL Server Reporting + Services or Power BI Report Server by calling the + `RemoveUnattendedExecutionAccount` method on the + `MSReportServer_ConfigurationSetting` CIM instance. + + After removing the unattended execution account, reports that require + credentials but have no user context (such as scheduled subscriptions + with data sources that require credentials) will fail. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER PassThru + If specified, returns the configuration CIM instance after removing + the unattended execution account. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSUnattendedExecutionAccount + + Removes the unattended execution account from the Reporting Services + instance. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSUnattendedExecutionAccount -Force + + Removes the unattended execution account without confirmation. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSUnattendedExecutionAccount -PassThru + + Removes the unattended execution account and returns the configuration + CIM instance. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + None. By default, this command does not generate any output. + + .OUTPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + When PassThru is specified, returns the MSReportServer_ConfigurationSetting + CIM instance. + + .NOTES + After removing the unattended execution account, scheduled reports + and subscriptions that rely on this account may fail. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-removeunattendedexecutionaccount +#> +function Remove-SqlDscRSUnattendedExecutionAccount +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $instanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Remove_SqlDscRSUnattendedExecutionAccount_Removing -f $instanceName) + + $descriptionMessage = $script:localizedData.Remove_SqlDscRSUnattendedExecutionAccount_ShouldProcessDescription -f $instanceName + $confirmationMessage = $script:localizedData.Remove_SqlDscRSUnattendedExecutionAccount_ShouldProcessConfirmation -f $instanceName + $captionMessage = $script:localizedData.Remove_SqlDscRSUnattendedExecutionAccount_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'RemoveUnattendedExecutionAccount' + } + + try + { + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Remove_SqlDscRSUnattendedExecutionAccount_FailedToRemove -f $instanceName + + $exception = New-Exception -Message $errorMessage -ErrorRecord $_ + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'RSRSUEA0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Set-SqlDscRSSslCertificateBinding.ps1 b/source/Public/Set-SqlDscRSSslCertificateBinding.ps1 index 3633ebc97..993259a28 100644 --- a/source/Public/Set-SqlDscRSSslCertificateBinding.ps1 +++ b/source/Public/Set-SqlDscRSSslCertificateBinding.ps1 @@ -94,7 +94,7 @@ function Set-SqlDscRSSslCertificateBinding { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - [OutputType([System.Object])] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] @@ -165,7 +165,7 @@ function Set-SqlDscRSSslCertificateBinding # Remove bindings that don't match the desired configuration foreach ($binding in $applicationBindings) { - $shouldRemove = $binding.CertificateHash -ne $normalizedHash -or + $shouldRemove = $binding.CertificateHash.ToLower() -ne $normalizedHash -or $binding.IPAddress -ne $IPAddress -or $binding.Port -ne $Port @@ -179,7 +179,7 @@ function Set-SqlDscRSSslCertificateBinding # Check if the desired binding already exists $bindingExists = $applicationBindings | Where-Object -FilterScript { - $_.CertificateHash -eq $normalizedHash -and + $_.CertificateHash.ToLower() -eq $normalizedHash -and $_.IPAddress -eq $IPAddress -and $_.Port -eq $Port } diff --git a/source/Public/Set-SqlDscRSUnattendedExecutionAccount.ps1 b/source/Public/Set-SqlDscRSUnattendedExecutionAccount.ps1 new file mode 100644 index 000000000..60b98c401 --- /dev/null +++ b/source/Public/Set-SqlDscRSUnattendedExecutionAccount.ps1 @@ -0,0 +1,160 @@ +<# + .SYNOPSIS + Sets the unattended execution account for SQL Server Reporting Services. + + .DESCRIPTION + Sets the unattended execution account for SQL Server Reporting Services + or Power BI Report Server by calling the `SetUnattendedExecutionAccount` + method on the `MSReportServer_ConfigurationSetting` CIM instance. + + The unattended execution account is used when running reports that + require credentials for data sources but no user credentials are + available, such as scheduled subscriptions. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER Credential + Specifies the credentials for the unattended execution account. This + account should have minimal permissions, only what is required to + access data sources. + + .PARAMETER PassThru + If specified, returns the configuration CIM instance after setting + the unattended execution account. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + $credential = Get-Credential + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSUnattendedExecutionAccount -Credential $credential + + Sets the unattended execution account for the Reporting Services instance. + + .EXAMPLE + $credential = Get-Credential + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Set-SqlDscRSUnattendedExecutionAccount -Configuration $config -Credential $credential -Force + + Sets the unattended execution account without confirmation. + + .EXAMPLE + $credential = Get-Credential + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSUnattendedExecutionAccount -Credential $credential -PassThru + + Sets the unattended execution account and returns the configuration + CIM instance. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + None. By default, this command does not generate any output. + + .OUTPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + When PassThru is specified, returns the MSReportServer_ConfigurationSetting + CIM instance. + + .NOTES + The Reporting Services service may need to be restarted for the + changes to take effect. + + The unattended execution account credentials are stored encrypted + in the report server database. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-setunattendedexecutionaccount +#> +function Set-SqlDscRSUnattendedExecutionAccount +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([Microsoft.Management.Infrastructure.CimInstance])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + # cSpell:ignore BSTR + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $instanceName = $Configuration.InstanceName + $userName = $Credential.UserName + + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUnattendedExecutionAccount_Setting -f $userName, $instanceName) + + $descriptionMessage = $script:localizedData.Set_SqlDscRSUnattendedExecutionAccount_ShouldProcessDescription -f $userName, $instanceName + $confirmationMessage = $script:localizedData.Set_SqlDscRSUnattendedExecutionAccount_ShouldProcessConfirmation -f $userName + $captionMessage = $script:localizedData.Set_SqlDscRSUnattendedExecutionAccount_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + $passwordBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) + + try + { + $passwordPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordBstr) + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'SetUnattendedExecutionAccount' + Arguments = @{ + UserName = $userName + Password = $passwordPlainText + } + } + + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Set_SqlDscRSUnattendedExecutionAccount_FailedToSet -f $instanceName + + $exception = New-Exception -Message $errorMessage -ErrorRecord $_ + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'SSRSUEA0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + finally + { + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordBstr) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/WikiSource/Deploy-Power-BI-Report-Server.md b/source/WikiSource/Deploy-Power-BI-Report-Server.md index fb3b07064..bd576bf05 100644 --- a/source/WikiSource/Deploy-Power-BI-Report-Server.md +++ b/source/WikiSource/Deploy-Power-BI-Report-Server.md @@ -42,6 +42,9 @@ Install-PSResource -Name 'SqlServerDsc' -Scope 'AllUsers' -TrustRepository # Install SqlServer module (required for SMO assemblies) Install-PSResource -Name 'SqlServer' -Version '22.2.0' -Scope 'AllUsers' -TrustRepository + +# Install PSPKI module (required for creating self-signed certificates) +Install-PSResource -Name 'PSPKI' -Scope 'AllUsers' -TrustRepository ``` @@ -295,7 +298,108 @@ Write-Information -MessageData "Error Dump Directory: $($setupConfig.ErrorDumpDi ``` -## Phase 4: Configure Report Server Database +## Phase 4: Create SSL Certificate + +Before configuring the report server, create an SSL/TLS certificate for +secure HTTPS access. This guide uses a self-signed certificate for demonstration +purposes. + +> [!IMPORTANT] +> **Production Certificate Recommendations:** +> +> For production environments, use a certificate issued by a trusted Certificate +> Authority (CA) instead of a self-signed certificate. Production certificates +> should meet the following requirements: +> +> - **Subject Name (CN)** or **Subject Alternative Name (SAN)**: Must match the +> server's fully qualified domain name (FQDN) that clients will use to access +> the report server +> - **Enhanced Key Usage (EKU)**: Must include "Server Authentication" +> (OID: 1.3.6.1.5.5.7.3.1) +> - **Key Length**: Minimum 2048 bits for RSA keys +> - **Validity Period**: Follow your organization's certificate lifecycle policy +> - **Trusted CA**: Issued by a CA that is trusted by all client machines +> +> You can obtain production certificates from: +> +> - Your organization's internal PKI/Certificate Authority +> - Public CAs such as DigiCert, Let's Encrypt, or GlobalSign +> - Azure Key Vault (for Azure-integrated environments) + +### Create a Self-Signed Certificate + +For development and testing, create a self-signed certificate using the PSPKI +module: + + +```powershell +# Import the PSPKI module for certificate creation +Import-Module -Name 'PSPKI' -ErrorAction 'Stop' + +# Get the computer name for the certificate subject +$computerName = [System.Net.Dns]::GetHostName() + +# Create a self-signed certificate for SSL/TLS +$newCertificateParams = @{ + Subject = "CN=$computerName" + EKU = 'Server Authentication' + KeyUsage = 'DigitalSignature, KeyEncipherment, DataEncipherment' + SAN = "dns:$computerName" + FriendlyName = 'Power BI Report Server SSL Certificate' + Exportable = $true + KeyLength = 2048 + ProviderName = 'Microsoft Software Key Storage Provider' + AlgorithmName = 'RSA' + SignatureAlgorithm = 'SHA256' + StoreLocation = 'LocalMachine' +} + +$sslCertificate = New-SelfSignedCertificateEx @newCertificateParams + +Write-Information -MessageData "Certificate created with thumbprint: $($sslCertificate.Thumbprint)" -InformationAction 'Continue' +``` + + +### Add Certificate to Trusted Root (Self-Signed Only) + +For self-signed certificates, add the certificate to the Trusted Root +Certification Authorities store to avoid browser trust warnings: + + +```powershell +# Export the certificate to a temporary file +$certificatePath = Join-Path -Path $env:TEMP -ChildPath 'PBIRS_SSL_Certificate.cer' +Export-Certificate -Cert $sslCertificate -FilePath $certificatePath -ErrorAction 'Stop' + +# Import into Trusted Root Certification Authorities +$null = Import-Certificate -FilePath $certificatePath -CertStoreLocation 'Cert:\LocalMachine\Root' -ErrorAction 'Stop' + +# Clean up the temporary file +Remove-Item -Path $certificatePath -Force + +Write-Information -MessageData 'Certificate added to Trusted Root Certification Authorities.' -InformationAction 'Continue' +``` + + +> [!NOTE] +> Adding a self-signed certificate to the Trusted Root store is only required +> for local testing. In production, your CA-issued certificate should already +> be trusted by client machines. + +### Store the Certificate Thumbprint + +Save the certificate thumbprint for use in later configuration steps: + + +```powershell +# Store the certificate thumbprint for later use +$certificateThumbprint = $sslCertificate.Thumbprint + +Write-Information -MessageData "Certificate thumbprint: $certificateThumbprint" -InformationAction 'Continue' +``` + + +## Phase 5: Configure Report Server Database After installation, configure Power BI Report Server to use the SQL Server instance for its database. @@ -314,29 +418,21 @@ Write-Information -MessageData "Is Initialized: $($rsConfig.IsInitialized)" -Inf ``` -### Configure Secure Connection +### Enable Secure Connection -By default, Power BI Report Server can be configured with or without SSL/TLS. -For this guide, we disable secure connection to use HTTP for simplicity: +Enable SSL/TLS to require secure HTTPS connections to the report server: ```powershell -# Disable secure connection (use HTTP) -$rsConfig | Disable-SqlDscRsSecureConnection -Force -ErrorAction 'Stop' +# Enable secure connection (require HTTPS) +$rsConfig | Enable-SqlDscRsSecureConnection -Force -ErrorAction 'Stop' -Write-Information -MessageData 'Secure connection disabled (using HTTP).' -InformationAction 'Continue' +Write-Information -MessageData 'Secure connection enabled (using HTTPS).' -InformationAction 'Continue' ``` -> [!IMPORTANT] -> For production environments, you should use `Enable-SqlDscRsSecureConnection` -> instead to enable HTTPS. This requires: -> -> - A valid SSL/TLS certificate installed on the server -> - The certificate thumbprint to pass to the command -> - Proper DNS configuration for the certificate's subject name -> -> Example: `$rsConfig | Enable-SqlDscRsSecureConnection -Force` +This sets the `SecureConnectionLevel` to 1 or higher, which requires all +client connections to use SSL/TLS encryption. ### Set Virtual Directories @@ -367,36 +463,69 @@ Write-Information -MessageData 'Virtual directory set for ReportServerWebApp (we ### Add URL Reservations -After setting the virtual directories, add URL reservations to register the -URLs. This allows the report server to listen on these URLs: +After setting the virtual directories, add URL reservations for HTTPS on port +443. This allows the report server to listen on secure URLs: > [!IMPORTANT] > URL reservations are registered for the service account. Changing the > service account requires updating all the URL reservations. - ```powershell -# Add URL reservation for the Report Server web service +# Add HTTPS URL reservation for the Report Server web service $rsConfig | Add-SqlDscRSUrlReservation ` -Application 'ReportServerWebService' ` - -UrlString 'http://+:80' ` + -UrlString 'https://+:443' ` -Force ` -ErrorAction 'Stop' -Write-Information -MessageData 'URL reservation added for ReportServerWebService.' -InformationAction 'Continue' +Write-Information -MessageData 'HTTPS URL reservation added for ReportServerWebService.' -InformationAction 'Continue' -# Add URL reservation for the web portal +# Add HTTPS URL reservation for the web portal $rsConfig | Add-SqlDscRSUrlReservation ` -Application 'ReportServerWebApp' ` - -UrlString 'http://+:80' ` + -UrlString 'https://+:443' ` + -Force ` + -ErrorAction 'Stop' + +Write-Information -MessageData 'HTTPS URL reservation added for ReportServerWebApp (web portal).' -InformationAction 'Continue' +``` + + +### Add SSL Certificate Bindings + +Bind the SSL certificate to both report server applications. This associates +the certificate with the HTTPS URL reservations: + + +```powershell +# Add SSL certificate binding for the Report Server web service +$rsConfig | Add-SqlDscRSSslCertificateBinding ` + -Application 'ReportServerWebService' ` + -CertificateHash $certificateThumbprint ` + -IPAddress '0.0.0.0' ` + -Port 443 ` + -Force ` + -ErrorAction 'Stop' + +Write-Information -MessageData 'SSL certificate bound to ReportServerWebService.' -InformationAction 'Continue' + +# Add SSL certificate binding for the web portal +$rsConfig | Add-SqlDscRSSslCertificateBinding ` + -Application 'ReportServerWebApp' ` + -CertificateHash $certificateThumbprint ` + -IPAddress '0.0.0.0' ` + -Port 443 ` -Force ` -ErrorAction 'Stop' -Write-Information -MessageData 'URL reservation added for ReportServerWebApp (web portal).' -InformationAction 'Continue' +Write-Information -MessageData 'SSL certificate bound to ReportServerWebApp (web portal).' -InformationAction 'Continue' ``` +The `-IPAddress '0.0.0.0'` binds the certificate to all available IP addresses +on the server. + ### Generate Database Scripts Power BI Report Server provides methods to generate the T-SQL scripts needed @@ -471,9 +600,10 @@ Write-Information -MessageData 'Database connection configured successfully.' -I ``` -## Phase 5: Initialize and Verify +## Phase 6: Initialize and Verify -Initialize the report server and verify that the web portal is accessible. +Initialize the report server and verify that the web portal is accessible +over HTTPS. ### Initialize the Report Server @@ -552,7 +682,7 @@ foreach ($result in $accessResults) If everything is configured correctly, you should see both the Report Server -web service and the web portal as accessible with HTTP status code 200. +web service and the web portal as accessible with HTTPS status code 200. ### Access the Web Portal @@ -560,15 +690,22 @@ Open the web portal in your browser: ```powershell -# Open the web portal in the default browser -Start-Process 'http://localhost/Reports' +# Open the web portal in the default browser (using HTTPS) +Start-Process 'https://localhost/Reports' ``` The default URLs are: -- **Web Portal**: `http://localhost/Reports` -- **Web Service**: `http://localhost/ReportServer` +- **Web Portal**: `https://localhost/Reports` +- **Web Service**: `https://localhost/ReportServer` + +> [!NOTE] +> If you used a self-signed certificate, your browser may display a certificate +> warning on first access. This is expected because the certificate was not +> issued by a publicly trusted Certificate Authority. You can proceed past the +> warning for testing purposes. For production deployments, use a CA-issued +> certificate to avoid these warnings. ## Cleanup @@ -649,7 +786,7 @@ immediately after starting. ### Web Portal Not Accessible After Initialization -**Symptoms**: `Test-SqlDscRSAccessible` returns `$false` or HTTP errors. +**Symptoms**: `Test-SqlDscRSAccessible` returns `$false` or HTTPS errors. **Solutions**: @@ -661,7 +798,18 @@ immediately after starting. ``` -1. Check Windows Firewall rules allow HTTP traffic on port 80. +1. Check Windows Firewall rules allow HTTPS traffic on port 443: + + + ```powershell + # Create a firewall rule to allow HTTPS traffic on port 443 + New-NetFirewallRule -DisplayName 'Power BI Report Server HTTPS' ` + -Direction Inbound ` + -Protocol TCP ` + -LocalPort 443 ` + -Action Allow + ``` + 1. Verify URL reservations are configured correctly: @@ -671,11 +819,19 @@ immediately after starting. ``` -1. Check if another application is using port 80: +1. Check if another application is using port 443: + + + ```powershell + netstat -ano | Select-String ':443 ' + ``` + + +1. Verify SSL certificate bindings are configured: ```powershell - netstat -ano | Select-String ':80 ' + $rsConfig | Get-SqlDscRSSslCertificateBinding ``` @@ -683,10 +839,68 @@ immediately after starting. After deploying Power BI Report Server, you may want to: -- Configure SSL/TLS certificates for HTTPS access +- Replace the self-signed certificate with a CA-issued certificate for production - Set up email delivery for subscriptions - Configure authentication providers - Create and publish your first Power BI report - Set up backup and recovery procedures For more information, see the [Power BI Report Server documentation](https://learn.microsoft.com/power-bi/report-server/). + +--- + +## Appendix: HTTP Configuration (Development Only) + +> [!WARNING] +> The following configuration uses unencrypted HTTP connections and should +> **only be used for local development or testing**. Never use HTTP in +> production environments as it exposes sensitive data to network interception. + +If you need to configure Power BI Report Server with HTTP instead of HTTPS +(for example, in an isolated development environment), follow these +alternative steps in place of the secure configuration above: + +### Disable Secure Connection + + +```powershell +# Disable secure connection (use HTTP instead of HTTPS) +$rsConfig | Disable-SqlDscRsSecureConnection -Force -ErrorAction 'Stop' + +Write-Information -MessageData 'Secure connection disabled (using HTTP).' -InformationAction 'Continue' +``` + + +### Add HTTP URL Reservations + + +```powershell +# Add HTTP URL reservation for the Report Server web service (port 80) +$rsConfig | Add-SqlDscRSUrlReservation ` + -Application 'ReportServerWebService' ` + -UrlString 'http://+:80' ` + -Force ` + -ErrorAction 'Stop' + +# Add HTTP URL reservation for the web portal (port 80) +$rsConfig | Add-SqlDscRSUrlReservation ` + -Application 'ReportServerWebApp' ` + -UrlString 'http://+:80' ` + -Force ` + -ErrorAction 'Stop' + +Write-Information -MessageData 'HTTP URL reservations added.' -InformationAction 'Continue' +``` + + +### Skip Certificate Binding + +When using HTTP, skip the SSL certificate creation and binding steps entirely. +The report server will be accessible at: + +- **Web Portal**: `http://localhost/Reports` +- **Web Service**: `http://localhost/ReportServer` + +> [!NOTE] +> Ensure Windows Firewall allows inbound traffic on port 80 if accessing +> from remote machines. diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 822b23d4c..c8f8fefab 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -303,6 +303,7 @@ ConvertFrom-StringData @' Get_SqlDscRSConfiguration_VersionNotFound = Could not determine the version for Reporting Services instance '{0}'. (GSRSCD0002) Get_SqlDscRSConfiguration_FailedToGetConfiguration = Failed to get the configuration CIM instance for Reporting Services instance '{0}': {1} (GSRSCD0003) Get_SqlDscRSConfiguration_ConfigurationNotFound = Could not find the configuration CIM instance for Reporting Services instance '{0}'. (GSRSCD0004) + Get_SqlDscRSConfiguration_RetryingAfterDelay = Configuration CIM instance not found for Reporting Services instance '{0}'. Attempt {1} of {2} failed. Retrying in {3} seconds. (GSRSCD0005) ## Get-SqlDscRSLogPath Get_SqlDscRSLogPath_GettingPath = Getting log file path for Reporting Services instance '{0}'. @@ -910,6 +911,22 @@ ConvertFrom-StringData @' Initialize_SqlDscRS_ShouldProcessCaption = Initialize Reporting Services instance Initialize_SqlDscRS_FailedToInitialize = Failed to initialize Reporting Services instance '{0}'. {1} (ISRS0001) + ## Set-SqlDscRSUnattendedExecutionAccount + Set_SqlDscRSUnattendedExecutionAccount_Setting = Setting unattended execution account to '{0}' for Reporting Services instance '{1}'. + Set_SqlDscRSUnattendedExecutionAccount_ShouldProcessDescription = Setting unattended execution account to '{0}' for Reporting Services instance '{1}'. + Set_SqlDscRSUnattendedExecutionAccount_ShouldProcessConfirmation = Are you sure you want to set the unattended execution account to '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Set_SqlDscRSUnattendedExecutionAccount_ShouldProcessCaption = Set unattended execution account for Reporting Services instance + Set_SqlDscRSUnattendedExecutionAccount_FailedToSet = Failed to set unattended execution account for Reporting Services instance '{0}'. (RSSUEA0001) + + ## Remove-SqlDscRSUnattendedExecutionAccount + Remove_SqlDscRSUnattendedExecutionAccount_Removing = Removing unattended execution account from Reporting Services instance '{0}'. + Remove_SqlDscRSUnattendedExecutionAccount_ShouldProcessDescription = Removing unattended execution account from Reporting Services instance '{0}'. + Remove_SqlDscRSUnattendedExecutionAccount_ShouldProcessConfirmation = Are you sure you want to remove the unattended execution account from Reporting Services instance '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Remove_SqlDscRSUnattendedExecutionAccount_ShouldProcessCaption = Remove unattended execution account from Reporting Services instance + Remove_SqlDscRSUnattendedExecutionAccount_FailedToRemove = Failed to remove unattended execution account for Reporting Services instance '{0}'. (RSRSUEA0001) + ## Get-SqlDscRSSslCertificate Get_SqlDscRSSslCertificate_Getting = Getting available SSL certificates for Reporting Services instance '{0}'. Get_SqlDscRSSslCertificate_FailedToGet = Failed to get available SSL certificates for Reporting Services instance '{0}'. {1} (GSRSSC0001) diff --git a/tests/Integration/Commands/Add-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 b/tests/Integration/Commands/Add-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 index 813a5afff..5b1eec971 100644 --- a/tests/Integration/Commands/Add-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Add-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 @@ -86,45 +86,45 @@ Describe 'Add-SqlDscRSSslCertificateBinding' { } } - Context 'When adding SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + Context 'When adding SSL certificate binding for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should add SSL certificate binding for ReportServerWebService' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } It 'Should add SSL certificate binding for ReportServerWebApp' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } } - Context 'When adding SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + Context 'When adding SSL certificate binding for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should add SSL certificate binding for ReportServerWebService' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } It 'Should add SSL certificate binding for ReportServerWebApp' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } } - Context 'When adding SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + Context 'When adding SSL certificate binding for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should add SSL certificate binding for ReportServerWebService' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } It 'Should add SSL certificate binding for ReportServerWebApp' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } } @@ -134,11 +134,11 @@ Describe 'Add-SqlDscRSSslCertificateBinding' { } It 'Should add SSL certificate binding for ReportServerWebService' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' @script:addSslCertificateBindingParameters } It 'Should add SSL certificate binding for ReportServerWebApp' { - { $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } | Should -Not -Throw + $script:configuration | Add-SqlDscRSSslCertificateBinding -Application 'ReportServerWebApp' @script:addSslCertificateBindingParameters } } } diff --git a/tests/Integration/Commands/Get-SqlDscRSSslCertificate.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSSslCertificate.Integration.Tests.ps1 index 694c8e66b..1a0c8994b 100644 --- a/tests/Integration/Commands/Get-SqlDscRSSslCertificate.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Get-SqlDscRSSslCertificate.Integration.Tests.ps1 @@ -55,7 +55,7 @@ BeforeAll { } Describe 'Get-SqlDscRSSslCertificate' { - Context 'When getting SSL certificates for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + Context 'When getting SSL certificates for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } @@ -93,7 +93,7 @@ Describe 'Get-SqlDscRSSslCertificate' { } } - Context 'When getting SSL certificates for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + Context 'When getting SSL certificates for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } @@ -131,7 +131,7 @@ Describe 'Get-SqlDscRSSslCertificate' { } } - Context 'When getting SSL certificates for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + Context 'When getting SSL certificates for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } diff --git a/tests/Integration/Commands/Get-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 index dbbc00f6f..08c16dd44 100644 --- a/tests/Integration/Commands/Get-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Get-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 @@ -57,7 +57,7 @@ BeforeAll { } Describe 'Get-SqlDscRSSslCertificateBinding' { - Context 'When getting SSL certificate bindings for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + Context 'When getting SSL certificate bindings for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } @@ -97,7 +97,7 @@ Describe 'Get-SqlDscRSSslCertificateBinding' { } } - Context 'When getting SSL certificate bindings for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + Context 'When getting SSL certificate bindings for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } @@ -137,7 +137,7 @@ Describe 'Get-SqlDscRSSslCertificateBinding' { } } - Context 'When getting SSL certificate bindings for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + Context 'When getting SSL certificate bindings for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } diff --git a/tests/Integration/Commands/Remove-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 index beff5ff27..adfc00395 100644 --- a/tests/Integration/Commands/Remove-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Remove-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 @@ -58,39 +58,37 @@ Describe 'Remove-SqlDscRSSslCertificateBinding' { } $script:testCertificateHash = $script:testCertificate.Thumbprint - $script:testIPAddress = '0.0.0.0' - $script:testPort = 443 Write-Verbose -Message ('Using self-signed certificate ''{0}'' with thumbprint ''{1}''.' -f $script:testCertificate.Subject, $script:testCertificateHash) -Verbose } - Context 'When removing SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + Context 'When removing SSL certificate binding for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should remove SSL certificate binding' { - { $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } } - Context 'When removing SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + Context 'When removing SSL certificate binding for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should remove SSL certificate binding' { - { $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } } - Context 'When removing SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + Context 'When removing SSL certificate binding for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should remove SSL certificate binding' { - { $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } } @@ -100,7 +98,7 @@ Describe 'Remove-SqlDscRSSslCertificateBinding' { } It 'Should remove SSL certificate binding' { - { $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Remove-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } } } diff --git a/tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1 new file mode 100644 index 000000000..b4569c1ea --- /dev/null +++ b/tests/Integration/Commands/Remove-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1 @@ -0,0 +1,80 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +<# + .NOTES + These tests validate removing the unattended execution account. + Since the unattended execution account may not be configured, + the tests may succeed or fail depending on the current state. +#> +Describe 'Remove-SqlDscRSUnattendedExecutionAccount' { + Context 'When removing unattended execution account for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should not throw when removing unattended execution account' { + $script:configuration | Remove-SqlDscRSUnattendedExecutionAccount -Force -ErrorAction 'Stop' + } + } + + Context 'When removing unattended execution account for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should not throw when removing unattended execution account' { + $script:configuration | Remove-SqlDscRSUnattendedExecutionAccount -Force -ErrorAction 'Stop' + } + } + + Context 'When removing unattended execution account for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should not throw when removing unattended execution account' { + $script:configuration | Remove-SqlDscRSUnattendedExecutionAccount -Force -ErrorAction 'Stop' + } + } + + Context 'When removing unattended execution account for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + } + + It 'Should not throw when removing unattended execution account' { + $script:configuration | Remove-SqlDscRSUnattendedExecutionAccount -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Set-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 b/tests/Integration/Commands/Set-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 index f445e95f4..3fedc19ab 100644 --- a/tests/Integration/Commands/Set-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Set-SqlDscRSSslCertificateBinding.Integration.Tests.ps1 @@ -59,19 +59,17 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } $script:testCertificateHash = $script:testCertificate.Thumbprint - $script:testIPAddress = '0.0.0.0' - $script:testPort = 443 Write-Verbose -Message ('Using self-signed certificate ''{0}'' with thumbprint ''{1}''.' -f $script:testCertificate.Subject, $script:testCertificateHash) -Verbose } - Context 'When setting SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + Context 'When setting SSL certificate binding for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should set SSL certificate binding' { - { $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } It 'Should return configuration when using PassThru' { @@ -83,13 +81,13 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } } - Context 'When setting SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + Context 'When setting SSL certificate binding for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should set SSL certificate binding' { - { $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } It 'Should return configuration when using PassThru' { @@ -101,13 +99,13 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } } - Context 'When setting SSL certificate binding for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + Context 'When setting SSL certificate binding for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } It 'Should set SSL certificate binding' { - { $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } It 'Should return configuration when using PassThru' { @@ -125,7 +123,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should set SSL certificate binding' { - { $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } | Should -Not -Throw + $script:configuration | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash $script:testCertificateHash -IPAddress $script:testIPAddress -Port $script:testPort -Force -ErrorAction 'Stop' } It 'Should return configuration when using PassThru' { diff --git a/tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1 b/tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1 new file mode 100644 index 000000000..1c75e4e5b --- /dev/null +++ b/tests/Integration/Commands/Set-SqlDscRSUnattendedExecutionAccount.Integration.Tests.ps1 @@ -0,0 +1,90 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +Describe 'Set-SqlDscRSUnattendedExecutionAccount' { + Context 'When setting unattended execution account for SQL Server Reporting Services 2017' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + $script:testUsername = '{0}\TestUnattendedAccount' -f (Get-ComputerName) + $script:testPassword = ConvertTo-SecureString -String 'P@ssw0rd123!' -AsPlainText -Force + $script:testCredential = [System.Management.Automation.PSCredential]::new($script:testUsername, $script:testPassword) + } + + It 'Should set unattended execution account' { + $script:configuration | Set-SqlDscRSUnattendedExecutionAccount -Credential $script:testCredential -Force -ErrorAction 'Stop' + } + } + + Context 'When setting unattended execution account for SQL Server Reporting Services 2019' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + $script:testUsername = '{0}\TestUnattendedAccount' -f (Get-ComputerName) + $script:testPassword = ConvertTo-SecureString -String 'P@ssw0rd123!' -AsPlainText -Force + $script:testCredential = [System.Management.Automation.PSCredential]::new($script:testUsername, $script:testPassword) + } + + It 'Should set unattended execution account' { + $script:configuration | Set-SqlDscRSUnattendedExecutionAccount -Credential $script:testCredential -Force -ErrorAction 'Stop' + } + } + + Context 'When setting unattended execution account for SQL Server Reporting Services 2022' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + $script:testUsername = '{0}\TestUnattendedAccount' -f (Get-ComputerName) + $script:testPassword = ConvertTo-SecureString -String 'P@ssw0rd123!' -AsPlainText -Force + $script:testCredential = [System.Management.Automation.PSCredential]::new($script:testUsername, $script:testPassword) + } + + It 'Should set unattended execution account' { + $script:configuration | Set-SqlDscRSUnattendedExecutionAccount -Credential $script:testCredential -Force -ErrorAction 'Stop' + } + } + + Context 'When setting unattended execution account for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + + $script:testUsername = '{0}\TestUnattendedAccount' -f (Get-ComputerName) + $script:testPassword = ConvertTo-SecureString -String 'P@ssw0rd123!' -AsPlainText -Force + $script:testCredential = [System.Management.Automation.PSCredential]::new($script:testUsername, $script:testPassword) + } + + It 'Should set unattended execution account' { + $script:configuration | Set-SqlDscRSUnattendedExecutionAccount -Credential $script:testCredential -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSConfiguration.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSConfiguration.Tests.ps1 index 5fa8c99ee..4f7404934 100644 --- a/tests/Unit/Public/Get-SqlDscRSConfiguration.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscRSConfiguration.Tests.ps1 @@ -44,6 +44,50 @@ AfterAll { } Describe 'Get-SqlDscRSConfiguration' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-InstanceName] [[-Version] ] [[-RetryCount] ] [[-RetryDelaySeconds] ] [-SkipRetry] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSConfiguration').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + + It 'Should have RetryCount default value of 1' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfiguration').Parameters['RetryCount'] + + $parameterInfo.Attributes.Where({ $_ -is [System.Management.Automation.ParameterAttribute] }).Mandatory | Should -BeFalse + + $defaultValue = $parameterInfo.Attributes.Where({ $_ -is [System.Management.Automation.PSDefaultValueAttribute] }).Value + + # Check via reflection since default values are set in the param block + $functionInfo = Get-Command -Name 'Get-SqlDscRSConfiguration' + $functionInfo.Parameters['RetryCount'].ParameterType | Should -Be ([System.Int32]) + } + + It 'Should have RetryDelaySeconds default value of 30' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfiguration').Parameters['RetryDelaySeconds'] + + $parameterInfo.Attributes.Where({ $_ -is [System.Management.Automation.ParameterAttribute] }).Mandatory | Should -BeFalse + $parameterInfo.ParameterType | Should -Be ([System.Int32]) + } + + It 'Should have SkipRetry as a switch parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfiguration').Parameters['SkipRetry'] + + $parameterInfo.SwitchParameter | Should -BeTrue + } + } + BeforeAll { InModuleScope -ScriptBlock { function script:Get-CimInstance @@ -209,10 +253,107 @@ Describe 'Get-SqlDscRSConfiguration' { Mock -CommandName Get-CimInstance -MockWith { return $mockCimInstance } + + Mock -CommandName Start-Sleep } - It 'Should throw a terminating error' { + It 'Should throw a terminating error after retrying' { { Get-SqlDscRSConfiguration -InstanceName 'SSRS' } | Should -Throw -ErrorId 'GSRSCD0004,Get-SqlDscRSConfiguration' + + # Should retry once (default RetryCount = 1) plus initial attempt = 2 calls + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 2 + Should -Invoke -CommandName Start-Sleep -ParameterFilter { + $Seconds -eq 30 + } -Exactly -Times 1 + } + + It 'Should throw immediately with -SkipRetry' { + { Get-SqlDscRSConfiguration -InstanceName 'SSRS' -SkipRetry } | Should -Throw -ErrorId 'GSRSCD0004,Get-SqlDscRSConfiguration' + + # Should not retry + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 + Should -Invoke -CommandName Start-Sleep -Exactly -Times 0 + } + } + + Context 'When configuration CIM instance is found on retry' { + BeforeAll { + $script:getCimInstanceCallCount = 0 + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject] @{ + InstanceName = 'SSRS' + CurrentVersion = '15.0.1103.41' + } + } + + Mock -CommandName Get-CimInstance -MockWith { + $script:getCimInstanceCallCount++ + + if ($script:getCimInstanceCallCount -eq 1) + { + # First call returns wrong instance + return [PSCustomObject] @{ + InstanceName = 'OtherInstance' + } + } + + # Second call returns correct instance + return [PSCustomObject] @{ + InstanceName = 'SSRS' + DatabaseServerName = 'localhost' + SecureConnectionLevel = 1 + } + } + + Mock -CommandName Start-Sleep + } + + AfterEach { + $script:getCimInstanceCallCount = 0 + } + + It 'Should succeed on retry' { + $result = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 2 + Should -Invoke -CommandName Start-Sleep -ParameterFilter { + $Seconds -eq 30 + } -Exactly -Times 1 + } + } + + Context 'When using custom retry parameters' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'OtherInstance' + } + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject] @{ + InstanceName = 'SSRS' + CurrentVersion = '15.0.1103.41' + } + } + + Mock -CommandName Get-CimInstance -MockWith { + return $mockCimInstance + } + + Mock -CommandName Start-Sleep + } + + It 'Should retry the specified number of times with specified delay' { + { Get-SqlDscRSConfiguration -InstanceName 'SSRS' -RetryCount 3 -RetryDelaySeconds 10 } | Should -Throw -ErrorId 'GSRSCD0004,Get-SqlDscRSConfiguration' + + # 1 initial + 3 retries = 4 calls + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 4 + Should -Invoke -CommandName Start-Sleep -ParameterFilter { + $Seconds -eq 10 + } -Exactly -Times 3 } } diff --git a/tests/Unit/Public/Get-SqlDscRSSslCertificateBinding.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSSslCertificateBinding.Tests.ps1 index 9675cb8d7..6b78f91c9 100644 --- a/tests/Unit/Public/Get-SqlDscRSSslCertificateBinding.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscRSSslCertificateBinding.Tests.ps1 @@ -171,4 +171,34 @@ Describe 'Get-SqlDscRSSslCertificateBinding' { Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 } } + + Context 'When specifying Lcid parameter explicitly' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + Application = @('ReportServerWebService') + CertificateHash = @('AABBCCDD') + IPAddress = @('0.0.0.0') + Port = @(443) + Lcid = @(1031) + } + } + } + + It 'Should use the specified Lcid' { + $result = $mockCimInstance | Get-SqlDscRSSslCertificateBinding -Lcid 1031 + + $result.Lcid | Should -Be 1031 + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.Lcid -eq 1031 + } -Exactly -Times 1 + + Should -Invoke -CommandName Get-OperatingSystem -Exactly -Times 0 + } + } } diff --git a/tests/Unit/Public/Remove-SqlDscRSSslCertificateBinding.Tests.ps1 b/tests/Unit/Public/Remove-SqlDscRSSslCertificateBinding.Tests.ps1 index afe9262f5..dc427d028 100644 --- a/tests/Unit/Public/Remove-SqlDscRSSslCertificateBinding.Tests.ps1 +++ b/tests/Unit/Public/Remove-SqlDscRSSslCertificateBinding.Tests.ps1 @@ -81,7 +81,7 @@ Describe 'Remove-SqlDscRSSslCertificateBinding' { } It 'Should remove SSL certificate binding without errors' { - { $mockCimInstance | Remove-SqlDscRSSslCertificateBinding -CertificateHash 'AABBCCDD' -Application 'ReportServerWebService' -Confirm:$false } | Should -Not -Throw + $mockCimInstance | Remove-SqlDscRSSslCertificateBinding -CertificateHash 'AABBCCDD' -Application 'ReportServerWebService' -Confirm:$false Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'RemoveSSLCertificateBindings' -and diff --git a/tests/Unit/Public/Remove-SqlDscRSUnattendedExecutionAccount.Tests.ps1 b/tests/Unit/Public/Remove-SqlDscRSUnattendedExecutionAccount.Tests.ps1 new file mode 100644 index 000000000..8ca278017 --- /dev/null +++ b/tests/Unit/Public/Remove-SqlDscRSUnattendedExecutionAccount.Tests.ps1 @@ -0,0 +1,170 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Remove-SqlDscRSUnattendedExecutionAccount' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Remove-SqlDscRSUnattendedExecutionAccount').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + } + + Context 'When removing unattended execution account successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove unattended execution account without errors' { + $mockCimInstance | Remove-SqlDscRSUnattendedExecutionAccount -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveUnattendedExecutionAccount' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Remove-SqlDscRSUnattendedExecutionAccount -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When removing unattended execution account with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Remove-SqlDscRSUnattendedExecutionAccount -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When removing unattended execution account with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove unattended execution account without confirmation' { + $mockCimInstance | Remove-SqlDscRSUnattendedExecutionAccount -Force + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method SetUnattendedExecutionAccount() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Remove-SqlDscRSUnattendedExecutionAccount -Confirm:$false } | Should -Throw -ErrorId 'RSRSUEA0001,Remove-SqlDscRSUnattendedExecutionAccount' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | Remove-SqlDscRSUnattendedExecutionAccount -WhatIf + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove unattended execution account' { + Remove-SqlDscRSUnattendedExecutionAccount -Configuration $mockCimInstance -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +} diff --git a/tests/Unit/Public/Set-SqlDscRSSslCertificateBinding.Tests.ps1 b/tests/Unit/Public/Set-SqlDscRSSslCertificateBinding.Tests.ps1 index 845084dc2..061392179 100644 --- a/tests/Unit/Public/Set-SqlDscRSSslCertificateBinding.Tests.ps1 +++ b/tests/Unit/Public/Set-SqlDscRSSslCertificateBinding.Tests.ps1 @@ -86,7 +86,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should add the SSL certificate binding' { - { $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false } | Should -Not -Throw + $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false Should -Invoke -CommandName Add-SqlDscRSSslCertificateBinding -Exactly -Times 1 Should -Invoke -CommandName Remove-SqlDscRSSslCertificateBinding -Exactly -Times 0 @@ -115,7 +115,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should remove existing and add new SSL certificate binding' { - { $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false } | Should -Not -Throw + $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false Should -Invoke -CommandName Remove-SqlDscRSSslCertificateBinding -Exactly -Times 1 Should -Invoke -CommandName Add-SqlDscRSSslCertificateBinding -Exactly -Times 1 @@ -144,7 +144,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should not add or remove any bindings' { - { $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false } | Should -Not -Throw + $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false Should -Invoke -CommandName Add-SqlDscRSSslCertificateBinding -Exactly -Times 0 Should -Invoke -CommandName Remove-SqlDscRSSslCertificateBinding -Exactly -Times 0 @@ -188,7 +188,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should set SSL certificate binding without confirmation' { - { $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Force } | Should -Not -Throw + $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Force Should -Invoke -CommandName Add-SqlDscRSSslCertificateBinding -Exactly -Times 1 } @@ -231,7 +231,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should set SSL certificate binding' { - { Set-SqlDscRSSslCertificateBinding -Configuration $mockCimInstance -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false } | Should -Not -Throw + Set-SqlDscRSSslCertificateBinding -Configuration $mockCimInstance -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -Confirm:$false Should -Invoke -CommandName Get-SqlDscRSSslCertificateBinding -Exactly -Times 1 } @@ -252,7 +252,7 @@ Describe 'Set-SqlDscRSSslCertificateBinding' { } It 'Should use custom IP address and port' { - { $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -IPAddress '192.168.1.1' -Port 8443 -Confirm:$false } | Should -Not -Throw + $mockCimInstance | Set-SqlDscRSSslCertificateBinding -Application 'ReportServerWebService' -CertificateHash 'AABBCCDD' -IPAddress '192.168.1.1' -Port 8443 -Confirm:$false Should -Invoke -CommandName Add-SqlDscRSSslCertificateBinding -ParameterFilter { $IPAddress -eq '192.168.1.1' -and $Port -eq 8443 diff --git a/tests/Unit/Public/Set-SqlDscRSUnattendedExecutionAccount.Tests.ps1 b/tests/Unit/Public/Set-SqlDscRSUnattendedExecutionAccount.Tests.ps1 new file mode 100644 index 000000000..2f31aa243 --- /dev/null +++ b/tests/Unit/Public/Set-SqlDscRSUnattendedExecutionAccount.Tests.ps1 @@ -0,0 +1,201 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Set-SqlDscRSUnattendedExecutionAccount' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] [-Credential] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Set-SqlDscRSUnattendedExecutionAccount').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + } + + Context 'When setting unattended execution account successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ExecutionAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set unattended execution account without errors' { + $mockCimInstance | Set-SqlDscRSUnattendedExecutionAccount -Credential $mockCredential -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetUnattendedExecutionAccount' -and + $Arguments.UserName -eq 'DOMAIN\ExecutionAccount' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Set-SqlDscRSUnattendedExecutionAccount -Credential $mockCredential -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When setting unattended execution account with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ExecutionAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Set-SqlDscRSUnattendedExecutionAccount -Credential $mockCredential -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When setting unattended execution account with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ExecutionAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set unattended execution account without confirmation' { + $mockCimInstance | Set-SqlDscRSUnattendedExecutionAccount -Credential $mockCredential -Force + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ExecutionAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method SetUnattendedExecutionAccount() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Set-SqlDscRSUnattendedExecutionAccount -Credential $mockCredential -Confirm:$false } | Should -Throw -ErrorId 'SSRSUEA0001,Set-SqlDscRSUnattendedExecutionAccount' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ExecutionAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | Set-SqlDscRSUnattendedExecutionAccount -Credential $mockCredential -WhatIf + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ExecutionAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set unattended execution account' { + Set-SqlDscRSUnattendedExecutionAccount -Configuration $mockCimInstance -Credential $mockCredential -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +}