diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e5c1e68a..4e716f2d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added public command `Get-SqlDscRSLogPath` to get the log file folder path + for SQL Server Reporting Services or Power BI Report Server. Returns the + ErrorDumpDirectory from the instance's setup configuration, which can be + used with `Get-ChildItem` and `Get-Content` to access service logs, portal + logs, and memory dumps. - Added public command `Test-SqlDscRSAccessible` to verify that SQL Server Reporting Services or Power BI Report Server web sites are accessible. Supports both CIM configuration input (with dynamic `-Site` parameter) and @@ -189,6 +194,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 for SQL Server Reporting Services or Power BI Report Server. Supports waiting for dependent services, configurable wait time, and accepts pipeline input from `Get-SqlDscRSConfiguration`. +- Added public commands `Get-SqlDscRSServiceAccount` and + `Set-SqlDscRSServiceAccount` to get and set the Windows service account for + SQL Server Reporting Services or Power BI Report Server. `Set-SqlDscRSServiceAccount` + wraps the `SetWindowsServiceIdentity` CIM method and supports updating encryption + key backups. - Added public command `Test-SqlDscRSInitialized` to test whether a Reporting Services instance is initialized by checking the `IsInitialized` property of the configuration CIM instance @@ -200,9 +210,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added public command `Request-SqlDscRSDatabaseUpgradeScript` to generate a T-SQL script for upgrading the report server database schema. Wraps the `GenerateDatabaseUpgradeScript` CIM method. +- Added wiki article `Troubleshooting-Report-Server` documenting how to + retrieve and analyze log files and Windows event logs for Power BI Report + Server and SQL Server Reporting Services. ### Changed +- SqlServerDsc + - Split the `Test_HQRM` pipeline job into two parallel jobs (`Test_QA` and + `Test_HQRM`) to reduce overall pipeline execution time by approximately + 15 minutes. - `Install-SqlDscServer` - **BREAKING CHANGE:** Removed `PrepareImage`, `Upgrade`, `EditionUpgrade`, `PrepareFailoverCluster`, and `InstallFailoverCluster` parameter sets. Use @@ -254,6 +271,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 of the deprecated private function `Get-ProtocolNameProperties` - `Class-Based Dsc Resource Tests` - Updated tests for ResourceBase 2.0. +- `Set-SqlDscRSUrlReservation` + - Added separate parameter set `Recreate` with the parameter `RecreateExisting` + to remove and re-add all existing URL reservations for all applications. + This is useful after changing the Windows service account, as URL reservations + are tied to a specific service account and must be recreated to use the new + account. The `Recreate` parameter set does not require `Application` or + `UrlString` parameters. +- Prerequisites Integration Tests + - Added `svc-RS` local Windows user for Reporting Services service account + integration testing. ### Fixed diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6fc256f73..942443f5c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -116,8 +116,8 @@ stages: Write-Host "Variable ShouldRunDscResourceIntegrationTests is set to: $shouldRun" pwsh: true - - job: Test_HQRM - displayName: 'HQRM' + - job: Test_QA + displayName: 'QA Test' pool: vmImage: 'windows-latest' timeoutInMinutes: '0' @@ -147,10 +147,25 @@ stages: Invoke-Pester -Configuration $pesterConfig name: qualityTest displayName: 'Run SqlServerDsc QA Test' + + - job: Test_HQRM + displayName: 'HQRM Test' + pool: + vmImage: 'windows-latest' + timeoutInMinutes: '0' + variables: + # This sets environment variable $env:SqlServerDscCI. + SqlServerDscCI: true + steps: + - task: DownloadPipelineArtifact@2 + displayName: 'Download Build Artifact' + inputs: + buildType: 'current' + artifactName: $(buildArtifactName) + targetPath: '$(Build.SourcesDirectory)/$(buildFolderName)' - task: PowerShell@2 name: test displayName: 'Run HQRM Test' - condition: succeededOrFailed() inputs: filePath: './build.ps1' arguments: '-Tasks hqrmtest' @@ -481,7 +496,7 @@ stages: - stage: Integration_Test_Commands_ReportingServices displayName: 'Integration Test Commands - Reporting Services' - dependsOn: Integration_Test_Commands_SqlServer + dependsOn: Build #Integration_Test_Commands_SqlServer jobs: - job: Test_Integration displayName: 'Commands' @@ -551,16 +566,28 @@ stages: 'tests/Integration/Commands/Get-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1' - 'tests/Integration/Commands/Restart-SqlDscRSService.Integration.Tests.ps1' 'tests/Integration/Commands/Request-SqlDscRSDatabaseUpgradeScript.Integration.Tests.ps1' + 'tests/Integration/Commands/Restart-SqlDscRSService.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInitialized.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1' # Group 4 'tests/Integration/Commands/Initialize-SqlDscRS.Integration.Tests.ps1' # Group 5 - Post-initialization validation 'tests/Integration/Commands/Post.Initialization.RS.Integration.Tests.ps1' + # Group 6 - Service account change + 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.EncryptedInformation.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1' + 'tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1' # Group 8 'tests/Integration/Commands/Repair-SqlDscReportingService.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1' # Group 9 'tests/Integration/Commands/Uninstall-SqlDscReportingService.Integration.Tests.ps1' ) @@ -576,7 +603,7 @@ stages: - stage: Integration_Test_Commands_BIReportServer displayName: 'Integration Test Commands - BI Report Server' - dependsOn: Integration_Test_Commands_SqlServer + dependsOn: Build #Integration_Test_Commands_SqlServer jobs: - job: Test_Integration displayName: 'Commands' @@ -637,12 +664,23 @@ stages: 'tests/Integration/Commands/Get-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1' + 'tests/Integration/Commands/Request-SqlDscRSDatabaseUpgradeScript.Integration.Tests.ps1' 'tests/Integration/Commands/Restart-SqlDscRSService.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInitialized.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1' # Group 4 'tests/Integration/Commands/Initialize-SqlDscRS.Integration.Tests.ps1' # Group 5 - Post-initialization validation 'tests/Integration/Commands/Post.Initialization.RS.Integration.Tests.ps1' + # Group 6 - Service account change + 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1' + 'tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1' + 'tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1' # Group 8 'tests/Integration/Commands/Repair-SqlDscPowerBIReportServer.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1' diff --git a/source/Public/Get-SqlDscRSConfiguration.ps1 b/source/Public/Get-SqlDscRSConfiguration.ps1 index 2af9d80b3..997f1cd58 100644 --- a/source/Public/Get-SqlDscRSConfiguration.ps1 +++ b/source/Public/Get-SqlDscRSConfiguration.ps1 @@ -9,9 +9,12 @@ Reporting Services configuration, such as `Enable-SqlDscRsSecureConnection` and `Disable-SqlDscRsSecureConnection`. - The configuration CIM instance provides access to properties like - `SecureConnectionLevel`, `DatabaseServerName`, `VirtualDirectoryReportServer`, - and methods for managing Reporting Services configuration. + The returned CIM instance provides access to properties documented in + [MSReportServer_ConfigurationSetting](https://learn.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/msreportserver-configurationsetting-properties), + such as `SecureConnectionLevel`, `DatabaseServerName`, + `VirtualDirectoryReportServer`, `WindowsServiceIdentityActual`, + 'WindowsServiceIdentityConfigured' and methods for managing Reporting + Services configuration. .PARAMETER InstanceName Specifies the name of the Reporting Services instance. This is a diff --git a/source/Public/Get-SqlDscRSLogPath.ps1 b/source/Public/Get-SqlDscRSLogPath.ps1 new file mode 100644 index 000000000..e47c012ac --- /dev/null +++ b/source/Public/Get-SqlDscRSLogPath.ps1 @@ -0,0 +1,123 @@ +<# + .SYNOPSIS + Gets the log file path for SQL Server Reporting Services or Power BI + Report Server. + + .DESCRIPTION + Gets the log file folder path for SQL Server Reporting Services (SSRS) + or Power BI Report Server (PBIRS). The returned path is the ErrorDumpDirectory + from the instance's setup configuration, which contains the LogFiles folder + where service logs, portal logs, and memory dumps are stored. + + The returned path can be used with `Get-ChildItem` and `Get-Content` to + access and read the log files. + + Common log file types in this folder: + - ReportingServicesService*.log - Web service activity logs + - RSPortal*.log - Portal access and activity logs + - SQLDumpr*.mdmp - Memory dumps for error analysis + + .PARAMETER InstanceName + Specifies the name of the Reporting Services instance. This is typically + 'SSRS' for SQL Server Reporting Services or 'PBIRS' for Power BI Report + Server. This parameter is mandatory when not passing a configuration object. + + .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. + + .EXAMPLE + Get-SqlDscRSLogPath -InstanceName 'SSRS' + + Returns the log file folder path for the SQL Server Reporting Services + instance 'SSRS'. + + .EXAMPLE + Get-SqlDscRSLogPath -InstanceName 'PBIRS' + + Returns the log file folder path for the Power BI Report Server + instance 'PBIRS'. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Get-SqlDscRSLogPath + + Gets the configuration for SSRS and pipes it to Get-SqlDscRSLogPath to + retrieve the log folder path. + + .EXAMPLE + Get-SqlDscRSLogPath -InstanceName 'SSRS' | Get-ChildItem -Filter '*.log' + + Gets the log path for SSRS and lists all .log files in that folder. + + .EXAMPLE + $logPath = Get-SqlDscRSLogPath -InstanceName 'SSRS' + Get-ChildItem -Path $logPath -Filter 'ReportingServicesService*.log' | + Sort-Object -Property LastWriteTime -Descending | + Select-Object -First 1 | + Get-Content -Tail 100 + + Gets the most recent web service log file and displays the last 100 lines. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + `System.String` + + Returns the path to the log files folder for the specified Reporting + Services instance. +#> +function Get-SqlDscRSLogPath +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(DefaultParameterSetName = 'ByInstanceName')] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'ByInstanceName')] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByConfiguration')] + [System.Object] + $Configuration + ) + + process + { + if ($PSCmdlet.ParameterSetName -eq 'ByConfiguration') + { + $InstanceName = $Configuration.InstanceName + } + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSLogPath_GettingPath -f $InstanceName) + + $setupConfiguration = Get-SqlDscRSSetupConfiguration -InstanceName $InstanceName + + if (-not $setupConfiguration) + { + $errorMessage = $script:localizedData.Get_SqlDscRSLogPath_InstanceNotFound -f $InstanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSLP0001' -ErrorCategory 'ObjectNotFound' -TargetObject $InstanceName + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + if ([System.String]::IsNullOrEmpty($setupConfiguration.ErrorDumpDirectory)) + { + $errorMessage = $script:localizedData.Get_SqlDscRSLogPath_LogPathNotFound -f $InstanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSLP0002' -ErrorCategory 'ObjectNotFound' -TargetObject $InstanceName + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSLogPath_FoundPath -f $setupConfiguration.ErrorDumpDirectory) + + return $setupConfiguration.ErrorDumpDirectory + } +} diff --git a/source/Public/Get-SqlDscRSServiceAccount.ps1 b/source/Public/Get-SqlDscRSServiceAccount.ps1 new file mode 100644 index 000000000..ca3324193 --- /dev/null +++ b/source/Public/Get-SqlDscRSServiceAccount.ps1 @@ -0,0 +1,71 @@ +<# + .SYNOPSIS + Gets the service account for SQL Server Reporting Services. + + .DESCRIPTION + Gets the Windows service account for SQL Server Reporting Services or + Power BI Report Server from the `MSReportServer_ConfigurationSetting` + CIM instance. + + This command returns the current service account name that is being + used by the Reporting Services Windows service. + + 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. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Get-SqlDscRSServiceAccount + + Gets the service account for the Reporting Services instance 'SSRS'. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Get-SqlDscRSServiceAccount -Configuration $config + + Gets the service account using a stored configuration object. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + `System.String` + + Returns the service account name. + + .NOTES + This is a convenience wrapper around accessing the + `WindowsServiceIdentityActual` property of the configuration CIM + instance. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/msreportserver-configurationsetting-properties +#> +function Get-SqlDscRSServiceAccount +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration + ) + + process + { + $instanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSServiceAccount_Getting -f $instanceName) + + return $Configuration.WindowsServiceIdentityActual + } +} diff --git a/source/Public/New-SqlDscRSEncryptionKey.ps1 b/source/Public/New-SqlDscRSEncryptionKey.ps1 new file mode 100644 index 000000000..026b34ba2 --- /dev/null +++ b/source/Public/New-SqlDscRSEncryptionKey.ps1 @@ -0,0 +1,136 @@ +<# + .SYNOPSIS + Generates a new encryption key for SQL Server Reporting Services. + + .DESCRIPTION + Generates a new encryption key for SQL Server Reporting Services or + Power BI Report Server by calling the `ReencryptSecureInformation` + method on the `MSReportServer_ConfigurationSetting` CIM instance. + + This command generates a new symmetric encryption key and re-encrypts + all stored secure information (such as credentials and connection + strings) with the new key. This operation is typically performed + when security requirements mandate key rotation. + + WARNING: After generating a new encryption key, you should immediately + back up the new key using `Backup-SqlDscRSEncryptionKey`. Any previous + encryption key backups will no longer be valid. + + 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 generating + the new encryption key. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | New-SqlDscRSEncryptionKey + + Generates a new encryption key for the Reporting Services instance. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | New-SqlDscRSEncryptionKey -Force + + Generates a new encryption key without confirmation. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | New-SqlDscRSEncryptionKey -PassThru + + Generates a new encryption key 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 + This operation invalidates any existing encryption key backups. + Immediately back up the new encryption key after this operation. + The Reporting Services service may need to be restarted after + generating a new encryption key. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-reencryptsecureinformation +#> +function New-SqlDscRSEncryptionKey +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([System.Object])] + 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.New_SqlDscRSEncryptionKey_Generating -f $instanceName) + + $descriptionMessage = $script:localizedData.New_SqlDscRSEncryptionKey_ShouldProcessDescription -f $instanceName + $confirmationMessage = $script:localizedData.New_SqlDscRSEncryptionKey_ShouldProcessConfirmation -f $instanceName + $captionMessage = $script:localizedData.New_SqlDscRSEncryptionKey_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'ReencryptSecureInformation' + } + + try + { + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + + Write-Warning -Message ($script:localizedData.New_SqlDscRSEncryptionKey_BackupReminder) + } + catch + { + $errorMessage = $script:localizedData.New_SqlDscRSEncryptionKey_FailedToGenerate -f $instanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru) -ErrorId 'NSRSEK0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Remove-SqlDscRSEncryptedInformation.ps1 b/source/Public/Remove-SqlDscRSEncryptedInformation.ps1 new file mode 100644 index 000000000..fcdd70e48 --- /dev/null +++ b/source/Public/Remove-SqlDscRSEncryptedInformation.ps1 @@ -0,0 +1,135 @@ +<# + .SYNOPSIS + Removes encrypted information from SQL Server Reporting Services. + + .DESCRIPTION + Removes all encrypted information stored in the SQL Server Reporting + Services or Power BI Report Server database by calling the + `DeleteEncryptedInformation` method on the + `MSReportServer_ConfigurationSetting` CIM instance. + + This command deletes all encrypted data stored in the report server + database, including stored credentials, connection strings, and + other sensitive data. + + WARNING: This is a destructive operation. After removing encrypted + information, stored credentials and connection strings are permanently + deleted and cannot be recovered. The report server will need to be + re-initialized and data sources will need to be reconfigured with + new credentials. + + 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 encrypted information. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSEncryptedInformation + + Removes all encrypted information from the Reporting Services instance. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSEncryptedInformation -Force + + Removes all encrypted information without confirmation. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSEncryptedInformation -PassThru + + Removes all encrypted information 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 + This is a destructive operation. Ensure you understand the impact + before removing encrypted information. The Reporting Services service + may need to be restarted after this operation. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-deleteencryptedinformation +#> +function Remove-SqlDscRSEncryptedInformation +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [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_SqlDscRSEncryptedInformation_Removing -f $instanceName) + + $descriptionMessage = $script:localizedData.Remove_SqlDscRSEncryptedInformation_ShouldProcessDescription -f $instanceName + $confirmationMessage = $script:localizedData.Remove_SqlDscRSEncryptedInformation_ShouldProcessConfirmation -f $instanceName + $captionMessage = $script:localizedData.Remove_SqlDscRSEncryptedInformation_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + try + { + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'DeleteEncryptedInformation' + } + + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $exception = New-Exception -Message ($script:localizedData.Remove_SqlDscRSEncryptedInformation_FailedToRemove -f $instanceName) -ErrorRecord $_ + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'RRSREI0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Remove-SqlDscRSEncryptionKey.ps1 b/source/Public/Remove-SqlDscRSEncryptionKey.ps1 new file mode 100644 index 000000000..b8699b104 --- /dev/null +++ b/source/Public/Remove-SqlDscRSEncryptionKey.ps1 @@ -0,0 +1,135 @@ +<# + .SYNOPSIS + Removes the encryption key from SQL Server Reporting Services. + + .DESCRIPTION + Removes the encryption key from SQL Server Reporting Services or + Power BI Report Server by calling the `DeleteEncryptionKey` method + on the `MSReportServer_ConfigurationSetting` CIM instance. + + This command deletes the current encryption key from the report + server. + + WARNING: This is a destructive operation. After removing the + encryption key, stored credentials and connection strings cannot + be decrypted. The report server will need to be re-initialized + with a new or restored encryption key. + + 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 encryption key. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSEncryptionKey + + Removes the encryption key from the Reporting Services instance. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSEncryptionKey -Force + + Removes the encryption key without confirmation. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Remove-SqlDscRSEncryptionKey -PassThru + + Removes the encryption key 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 + This is a destructive operation. Ensure you have a backup of the + encryption key before removing it. The Reporting Services service + may need to be restarted after this operation. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-deleteencryptionkey +#> +function Remove-SqlDscRSEncryptionKey +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [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_SqlDscRSEncryptionKey_Removing -f $instanceName) + + $descriptionMessage = $script:localizedData.Remove_SqlDscRSEncryptionKey_ShouldProcessDescription -f $instanceName + $confirmationMessage = $script:localizedData.Remove_SqlDscRSEncryptionKey_ShouldProcessConfirmation -f $instanceName + $captionMessage = $script:localizedData.Remove_SqlDscRSEncryptionKey_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + try + { + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'DeleteEncryptionKey' + Arguments = @{ + InstallationID = $Configuration.InstallationID + } + } + + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $exception = New-Exception -Message ($script:localizedData.Remove_SqlDscRSEncryptionKey_FailedToRemove -f $instanceName) -ErrorRecord $_ + + $errorRecord = New-ErrorRecord -Exception $exception -ErrorId 'RRSEK0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 b/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 index c7118a950..6abf633b6 100644 --- a/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 +++ b/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 @@ -32,6 +32,11 @@ If specified, indicates that the database is on a remote server. By default, assumes the database is local. + When specified with Windows authentication (default), the UserName must + be in the format `\`. See the Microsoft documentation + for more information about username requirements: + https://learn.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-generatedatabaserightsscript + .PARAMETER UseSqlAuthentication If specified, indicates the user is a SQL Server authentication user. By default, assumes Windows authentication. @@ -113,6 +118,17 @@ function Request-SqlDscRSDatabaseRightsScript { $rsInstanceName = $Configuration.InstanceName + # Validate UserName format when IsRemote is used with Windows authentication + if ($IsRemote.IsPresent -and -not $UseSqlAuthentication.IsPresent) + { + if ($UserName -notmatch '^[^\\]+\\[^\\]+$') + { + $errorMessage = $script:localizedData.Request_SqlDscRSDatabaseRightsScript_InvalidUserNameFormat -f $UserName + + New-ArgumentException -ArgumentName 'UserName' -Message $errorMessage + } + } + Write-Verbose -Message ($script:localizedData.Request_SqlDscRSDatabaseRightsScript_Generating -f $DatabaseName, $UserName, $rsInstanceName) $invokeRsCimMethodParameters = @{ diff --git a/source/Public/Set-SqlDscRSServiceAccount.ps1 b/source/Public/Set-SqlDscRSServiceAccount.ps1 new file mode 100644 index 000000000..2ae363989 --- /dev/null +++ b/source/Public/Set-SqlDscRSServiceAccount.ps1 @@ -0,0 +1,213 @@ +<# + .SYNOPSIS + Sets the service account for SQL Server Reporting Services. + + .DESCRIPTION + Sets the Windows service account for SQL Server Reporting Services or + Power BI Report Server by calling the `SetWindowsServiceIdentity` + method on the `MSReportServer_ConfigurationSetting` CIM instance. + + This command changes the Windows service account that the Reporting + Services service runs under. Sets file permissions on files and folders + in the report server installation directory. The account requires + LogonAsService rights in Windows, the specified account will be granted + this right. + + 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 new service account. The username + should be in the format 'DOMAIN\Username' for domain accounts or + 'Username' for local accounts. + + .PARAMETER UseBuiltInAccount + Indicates that the account specified is a built-in Windows account + such as 'NT AUTHORITY\NetworkService' or 'NT AUTHORITY\LocalSystem'. + When this switch is used, only the username portion of the Credential + is used and the password is ignored. + + .PARAMETER RestartService + If specified, restarts the Reporting Services service after changing + the service account. The service must be restarted for the change to + take effect. + + .PARAMETER PassThru + If specified, returns the configuration CIM instance after setting + the service account. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .PARAMETER SuppressUrlReservationWarning + If specified, suppresses the warning message about URL reservations + needing to be updated when the service account changes. + + .EXAMPLE + $credential = Get-Credential + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSServiceAccount -Credential $credential -RestartService + + Sets the service account for Reporting Services and restarts the service. + + .EXAMPLE + $credential = New-Object System.Management.Automation.PSCredential('NT AUTHORITY\NetworkService', (New-Object System.Security.SecureString)) + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSServiceAccount -Credential $credential -UseBuiltInAccount -Force + + Sets the service account to NetworkService without confirmation. + + .EXAMPLE + $credential = Get-Credential + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSServiceAccount -Credential $credential -PassThru + + Sets the service 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 must be restarted for the change to take + effect. Use the -RestartService parameter or manually restart the service. + + URL reservations are created for the current service account. + Changing the service account requires updating all URL + reservations. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-setwindowsserviceidentity +#> +function Set-SqlDscRSServiceAccount +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([System.Object])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseBuiltInAccount, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $RestartService, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $SuppressUrlReservationWarning + ) + + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $instanceName = $Configuration.InstanceName + $serviceName = $Configuration.ServiceName + + $userName = $Credential.UserName + + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSServiceAccount_Setting -f $userName, $instanceName) + + $descriptionMessage = $script:localizedData.Set_SqlDscRSServiceAccount_ShouldProcessDescription -f $userName, $instanceName + $confirmationMessage = $script:localizedData.Set_SqlDscRSServiceAccount_ShouldProcessConfirmation -f $userName + $captionMessage = $script:localizedData.Set_SqlDscRSServiceAccount_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + $passwordPlainText = '' + + if (-not $UseBuiltInAccount.IsPresent) + { + $passwordBstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($Credential.Password) + + try + { + $passwordPlainText = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($passwordBstr) + } + finally + { + [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($passwordBstr) + } + } + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'SetWindowsServiceIdentity' + Arguments = @{ + UseBuiltInAccount = $UseBuiltInAccount.IsPresent + Account = $userName + Password = $passwordPlainText + } + } + + $currentServiceAccount = $Configuration.WindowsServiceIdentityActual + + try + { + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + + if (-not $SuppressUrlReservationWarning.IsPresent -and $currentServiceAccount -ne $userName) + { + Write-Warning -Message ($script:localizedData.Set_SqlDscRSServiceAccount_UrlReservationWarning -f $currentServiceAccount, $userName) + } + + if ($RestartService.IsPresent) + { + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSServiceAccount_RestartingService -f $serviceName) + + Restart-SqlDscRSService -ServiceName $serviceName -Force + } + } + catch + { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + ($script:localizedData.Set_SqlDscRSServiceAccount_FailedToSet -f $instanceName, $_.Exception.Message), + 'SSRSSA0001', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Configuration + ) + ) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Set-SqlDscRSUrlReservation.ps1 b/source/Public/Set-SqlDscRSUrlReservation.ps1 index 79bf86b7d..0c7981de8 100644 --- a/source/Public/Set-SqlDscRSUrlReservation.ps1 +++ b/source/Public/Set-SqlDscRSUrlReservation.ps1 @@ -1,13 +1,22 @@ <# .SYNOPSIS Sets the URL reservations for a SQL Server Reporting Services or Power BI - Report Server application to the specified list. + Report Server application to the specified list, or recreates all existing + URL reservations. .DESCRIPTION - The `Set-SqlDscRSUrlReservation` command ensures that only the specified - URL reservations exist for the given application. It removes any existing - URL reservations that are not in the specified list and adds any URLs that - are not currently reserved. + The `Set-SqlDscRSUrlReservation` command can operate in two modes: + + **Set Mode (default):** Ensures that only the specified URL reservations + exist for the given application. It removes any existing URL reservations + that are not in the specified list and adds any URLs that are not currently + reserved. + + **Recreate Mode:** When using the `-RecreateExisting` parameter, the command + removes and re-adds all existing URL reservations for all applications. + This is useful after changing the Windows service account, as URL reservations + are tied to a specific service account and must be recreated to use the + new account. This command uses the `Get-SqlDscRSUrlReservation`, `Add-SqlDscRSUrlReservation`, and `Remove-SqlDscRSUrlReservation` commands internally. @@ -19,14 +28,19 @@ .PARAMETER Application Specifies the Reporting Services application for which to set URL reservations. Valid values are: ReportServerWebService, ReportServerWebApp, ReportManager. + This parameter is only used in Set mode. .PARAMETER UrlString Specifies one or more URL strings to reserve. Any existing URL reservations for the application that are not in this list will be removed. + This parameter is only used in Set mode. .PARAMETER Lcid Specifies the locale identifier (LCID) for the URL reservation. If not - specified, the operating system language code is used. + specified, the operating system language code is used. Note that the + LCID used when creating a URL reservation is not stored or retrievable, + so when using `-RecreateExisting`, the LCID cannot be determined from + the existing reservations and defaults to the OS language. .PARAMETER PassThru If specified, returns the Reporting Services configuration CIM instance. @@ -34,6 +48,13 @@ .PARAMETER Force If specified, suppresses the confirmation prompt. + .PARAMETER RecreateExisting + If specified, removes and re-adds all existing URL reservations for all + applications. This is useful after changing the Windows service account, + as URL reservations are tied to a specific service account and must be + recreated to use the new account. This parameter cannot be used with + `-Application` or `-UrlString`. + .INPUTS Microsoft.Management.Infrastructure.CimInstance @@ -63,6 +84,14 @@ Sets the URL reservations for the ReportServerWebApp application on a Power BI Report Server instance and returns the configuration object. + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + $config | Set-SqlDscRSUrlReservation -RecreateExisting -Force + + Recreates all existing URL reservations for all applications. This is + useful after changing the service account to update the reservations to + use the new account. + .NOTES This command calls the ReserveUrl and RemoveURL methods on the MSReportServer_ConfigurationSetting CIM class. @@ -85,7 +114,7 @@ function Set-SqlDscRSUrlReservation { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = 'Set')] [OutputType([System.Object])] param ( @@ -93,12 +122,12 @@ function Set-SqlDscRSUrlReservation [System.Object] $Configuration, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'Set')] [ValidateSet('ReportServerWebService', 'ReportServerWebApp', 'ReportManager')] [System.String] $Application, - [Parameter(Mandatory = $true)] + [Parameter(Mandatory = $true, ParameterSetName = 'Set')] [System.String[]] $UrlString, @@ -112,7 +141,11 @@ function Set-SqlDscRSUrlReservation [Parameter()] [System.Management.Automation.SwitchParameter] - $Force + $Force, + + [Parameter(Mandatory = $true, ParameterSetName = 'Recreate')] + [System.Management.Automation.SwitchParameter] + $RecreateExisting ) process @@ -124,70 +157,120 @@ function Set-SqlDscRSUrlReservation $instanceName = $Configuration.InstanceName - $verboseDescriptionMessage = $script:localizedData.Set_SqlDscRSUrlReservation_ShouldProcessVerboseDescription -f $Application, $instanceName - $verboseWarningMessage = $script:localizedData.Set_SqlDscRSUrlReservation_ShouldProcessVerboseWarning -f $Application, $instanceName - $captionMessage = $script:localizedData.Set_SqlDscRSUrlReservation_ShouldProcessCaption + # Determine ShouldProcess messages based on parameter set + if ($PSCmdlet.ParameterSetName -eq 'Recreate') + { + $verboseDescriptionMessage = $script:localizedData.Set_SqlDscRSUrlReservation_Recreate_ShouldProcessVerboseDescription -f $instanceName + $verboseWarningMessage = $script:localizedData.Set_SqlDscRSUrlReservation_Recreate_ShouldProcessVerboseWarning -f $instanceName + $captionMessage = $script:localizedData.Set_SqlDscRSUrlReservation_Recreate_ShouldProcessCaption + } + else + { + $verboseDescriptionMessage = $script:localizedData.Set_SqlDscRSUrlReservation_ShouldProcessVerboseDescription -f $Application, $instanceName + $verboseWarningMessage = $script:localizedData.Set_SqlDscRSUrlReservation_ShouldProcessVerboseWarning -f $Application, $instanceName + $captionMessage = $script:localizedData.Set_SqlDscRSUrlReservation_ShouldProcessCaption + } if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) { # Get current URL reservations $currentReservations = $Configuration | Get-SqlDscRSUrlReservation -ErrorAction 'Stop' - # Build a list of current URLs for the specified application - $currentUrls = @() - - if ($null -ne $currentReservations.Application -and $null -ne $currentReservations.UrlString) + if ($PSCmdlet.ParameterSetName -eq 'Recreate') { - for ($i = 0; $i -lt $currentReservations.Application.Count; $i++) + if ($null -eq $currentReservations.Application -or $currentReservations.Application.Count -eq 0) + { + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_NoReservationsToRecreate -f $instanceName) + } + else { - if ($currentReservations.Application[$i] -eq $Application) + # Build common parameters for Add/Remove commands + $commonParams = @{ + Force = $true + ErrorAction = 'Stop' + } + + <# + Note: LCID is not returned by ListReservedUrls, so we cannot determine + the original LCID. If not specified, Add-SqlDscRSUrlReservation will + use default. + #> + if ($PSBoundParameters.ContainsKey('Lcid')) { - $currentUrls += $currentReservations.UrlString[$i] + $commonParams['Lcid'] = $Lcid + } + + # Recreate all existing URL reservations + for ($i = 0; $i -lt $currentReservations.Application.Count; $i++) + { + $currentApplication = $currentReservations.Application[$i] + $currentUrl = $currentReservations.UrlString[$i] + + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_RecreatingUrl -f $currentUrl, $currentApplication, $instanceName) + + $Configuration | Remove-SqlDscRSUrlReservation @commonParams -Application $currentApplication -UrlString $currentUrl + $Configuration | Add-SqlDscRSUrlReservation @commonParams -Application $currentApplication -UrlString $currentUrl } } } + else + { + # Set parameter set - Build a list of current URLs for the specified application + $currentUrls = @() - Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_CurrentUrls -f $Application, ($currentUrls -join ', ')) - Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_DesiredUrls -f $Application, ($UrlString -join ', ')) + if ($null -ne $currentReservations.Application -and $null -ne $currentReservations.UrlString) + { + for ($i = 0; $i -lt $currentReservations.Application.Count; $i++) + { + if ($currentReservations.Application[$i] -eq $Application) + { + $currentUrls += $currentReservations.UrlString[$i] + } + } + } - # Determine URLs to remove (in current but not in desired) - $urlsToRemove = $currentUrls | Where-Object -FilterScript { $_ -notin $UrlString } + Write-Debug -Message ($script:localizedData.Set_SqlDscRSUrlReservation_CurrentUrls -f $Application, ($currentUrls -join ', ')) + Write-Debug -Message ($script:localizedData.Set_SqlDscRSUrlReservation_DesiredUrls -f $Application, ($UrlString -join ', ')) - # Determine URLs to add (in desired but not in current) - $urlsToAdd = $UrlString | Where-Object -FilterScript { $_ -notin $currentUrls } + # Determine URLs to remove (in current but not in desired) + $urlsToRemove = $currentUrls | Where-Object -FilterScript { $_ -notin $UrlString } - # Build common parameters for Add/Remove commands - $commonParams = @{ - Application = $Application - Force = $true - ErrorAction = 'Stop' - } + # Determine URLs to add (in desired but not in current) + $urlsToAdd = $UrlString | Where-Object -FilterScript { $_ -notin $currentUrls } - if ($PSBoundParameters.ContainsKey('Lcid')) - { - $commonParams['Lcid'] = $Lcid - } + # Build common parameters for Add/Remove commands + $commonParams = @{ + Application = $Application + Force = $true + ErrorAction = 'Stop' + } - # Remove URLs that should not exist - foreach ($urlToRemove in $urlsToRemove) - { - Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_RemovingUrl -f $urlToRemove, $Application, $instanceName) + if ($PSBoundParameters.ContainsKey('Lcid')) + { + $commonParams['Lcid'] = $Lcid + } - $Configuration | Remove-SqlDscRSUrlReservation @commonParams -UrlString $urlToRemove - } + # Remove URLs that should not exist + foreach ($urlToRemove in $urlsToRemove) + { + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_RemovingUrl -f $urlToRemove, $Application, $instanceName) - # Add URLs that should exist - foreach ($urlToAdd in $urlsToAdd) - { - Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_AddingUrl -f $urlToAdd, $Application, $instanceName) + $Configuration | Remove-SqlDscRSUrlReservation @commonParams -UrlString $urlToRemove + } - $Configuration | Add-SqlDscRSUrlReservation @commonParams -UrlString $urlToAdd - } + # Add URLs that should exist + foreach ($urlToAdd in $urlsToAdd) + { + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSUrlReservation_AddingUrl -f $urlToAdd, $Application, $instanceName) - if ($PassThru.IsPresent) - { - return $Configuration + $Configuration | Add-SqlDscRSUrlReservation @commonParams -UrlString $urlToAdd + } } } + + if ($PassThru.IsPresent) + { + return $Configuration + } } } diff --git a/source/WikiSource/Change-Report-Server-Service-Account.md b/source/WikiSource/Change-Report-Server-Service-Account.md new file mode 100644 index 000000000..6a4e7af9a --- /dev/null +++ b/source/WikiSource/Change-Report-Server-Service-Account.md @@ -0,0 +1,525 @@ +--- +Category: How-to +--- + +# Change Report Server Service Account + +This guide walks you through changing the service account for _Power BI Report +Server_ (PBIRS) or _SQL Server Reporting Services_ (SSRS) using _SqlServerDsc_ +PowerShell commands. Changing the service account is a multi-step process that +requires careful handling of encryption keys, database permissions, and URL +reservations. + +> [!NOTE] +> The examples in this guide use the instance name `'SSRS'` for _SQL Server +> Reporting Services_. If you are using _Power BI Report Server_ or have a +> custom instance name, substitute `'SSRS'` with your instance name (e.g., +> `'PBIRS'` or your custom name). + +## Why Is This Process Complex? + +When you change the Report Server service account, three interconnected systems +are affected: + +1. **Encryption Keys** — Report Server uses a symmetric encryption key to protect + sensitive data stored in the database (such as stored credentials for data + sources and connection strings). This key is tied to the service account's + Windows security context. The new account cannot decrypt data encrypted by + the old account. + +1. **Database Permissions** — The Report Server databases (`ReportServer` and + `ReportServerTempDB`) grant permissions to the service account. The new + account has no access until you explicitly grant it. + +1. **URL Reservations** — URL reservations in HTTP.sys are registered with the + service account's Security Identifier (SID). After changing accounts, the + old reservations reference a SID that no longer matches the running service. + +Failing to address any of these will leave your Report Server in a broken state. + +## Prerequisites + +Before starting, ensure you have: + +- **SqlServerDsc module installed** — Install from PowerShell Gallery: + + + ```powershell + Install-PSResource -Name 'SqlServerDsc' -Scope 'AllUsers' -TrustRepository + ``` + + +- **An existing, initialized Report Server instance** — The instance must be + fully configured with database connection established and the server initialized. + +- **The new service account created** — The Windows or Active Directory account + must exist before you begin. For domain accounts, use the format `DOMAIN\Username`. + +- **SQL Server access** — You need permissions to create logins and execute + scripts on the SQL Server instance hosting the Report Server databases. + +- **Administrator privileges** — Run PowerShell as Administrator on the Report + Server machine. + +## The Complete Workflow + +The service account change process consists of eight steps that must be executed +in order: + +| Step | Action | Command | +|------|--------|---------| +| 1 | Change the service account | `Set-SqlDscRSServiceAccount` | +| 2 | Verify the change | `Get-SqlDscRSServiceAccount` | +| 3 | Grant database permissions | `New-SqlDscLogin`, `Request-SqlDscRSDatabaseRightsScript`, `Invoke-SqlDscQuery` | +| 4 | Remove the old encryption key | `Remove-SqlDscRSEncryptionKey` | +| 5 | Create a new encryption key | `New-SqlDscRSEncryptionKey` | +| 6 | Recreate URL reservations | `Set-SqlDscRSUrlReservation -RecreateExisting` | +| 7 | Re-initialize the Report Server | `Initialize-SqlDscRS` | +| 8 | Validate accessibility | `Test-SqlDscRSAccessible` | + +## Step 1: Change the Service Account + +First, obtain the Report Server configuration and change the service account +using `Set-SqlDscRSServiceAccount`. + + +```powershell +# Get the Report Server configuration +$configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + +# Prepare the new service account credentials +$newCredential = Get-Credential -Message 'Enter the new service account credentials (DOMAIN\Username)' + +# Change the service account +$setRSServiceAccountParams = @{ + Credential = $newCredential + RestartService = $true + SuppressUrlReservationWarning = $true + Force = $true +} + +$configuration | Set-SqlDscRSServiceAccount @setRSServiceAccountParams +``` + + +**Parameters explained:** + +- `-Credential` — A `PSCredential` object containing the new service account + username and password. +- `-RestartService` — Automatically restarts the Report Server service after + the change. +- `-SuppressUrlReservationWarning` — Suppresses the warning about URL reservations + needing to be updated (we handle this in Step 6). +- `-Force` — Skips confirmation prompts for automation scenarios. + +**What happens internally:** + +This command calls the WMI method `SetWindowsServiceIdentity`, which: + +- Updates the Windows service to run under the new account +- Sets appropriate file permissions on the Report Server installation directory +- Grants the `LogonAsService` right to the new account + +> [!IMPORTANT] +> After this step, the new account does NOT yet have database access, and the +> encryption key is still tied to the old account. The Report Server will not +> function correctly until all remaining steps are completed. + +## Step 2: Verify the Service Account Change + +Confirm that the service account was changed successfully: + + +```powershell +# Refresh the configuration object +$configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + +# Get the current service account +$currentServiceAccount = $configuration | Get-SqlDscRSServiceAccount + +Write-Information -MessageData "Service account is now: $currentServiceAccount" -InformationAction 'Continue' +``` + + +The returned value should match the username you specified in Step 1. + +## Step 3: Grant Database Permissions + +The new service account needs permissions to access the Report Server databases. +This involves three sub-steps. + +### 3a. Create a SQL Server Login for the New Account + +Connect to the SQL Server instance hosting the Report Server databases and create +a login for the new service account: + + +```powershell +# Connect to the SQL Server hosting the RS databases +$serverObject = Connect-SqlDscDatabaseEngine -ServerName 'localhost' -InstanceName 'RSDB' + +# Create a Windows login for the new service account +New-SqlDscLogin -ServerObject $serverObject -Name $currentServiceAccount -WindowsUser -Force + +# Disconnect from the server +Disconnect-SqlDscDatabaseEngine -ServerObject $serverObject +``` + + +> [!NOTE] +> Replace `'localhost'` and `'RSDB'` with your actual SQL Server name and instance +> name. If using the default instance, omit `-InstanceName` or use `'MSSQLSERVER'`. + +### 3b. Generate and Execute the Database Rights Script + +Request the database rights script from Report Server and execute it: + + +```powershell +# Get the database name from the configuration +$databaseName = $configuration.DatabaseName + +# Generate the T-SQL script that grants required permissions +$databaseRightsScript = $configuration | Request-SqlDscRSDatabaseRightsScript ` + -DatabaseName $databaseName ` + -UserName $currentServiceAccount + +# Execute the script on the SQL Server +Invoke-SqlDscQuery ` + -ServerName 'localhost' ` + -InstanceName 'RSDB' ` + -DatabaseName 'master' ` + -Query $databaseRightsScript ` + -Force +``` + + +> [!NOTE] +> For more details on the `-UserName` parameter and the permissions granted, +> see [GenerateDatabaseRightsScript Method](https://learn.microsoft.com/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-generatedatabaserightsscript). + +### 3c. Restart the Report Server Service + +Restart the service to apply the new database permissions: + +```powershell +$configuration | Restart-SqlDscRSService -Force +``` + +After this step, the Report Server can connect to its databases using the new +service account. + +## Step 4: Remove the Old Encryption Key + +> [!WARNING] +> This is a destructive operation. Once you remove the encryption key, the old +> key cannot be recovered. Any data encrypted with the old key (stored credentials, +> connection strings) will be inaccessible until a new key is generated. + +Remove the existing encryption key that was tied to the old service account: + +```powershell +$configuration | Remove-SqlDscRSEncryptionKey -Force +``` + +This command calls the WMI method `DeleteEncryptionKey`, which removes the +symmetric encryption key from the Report Server. + +**What this affects:** + +- Stored credentials for data sources +- Connection strings with embedded credentials +- Subscription delivery settings with credentials + +After removing the key, the Report Server cannot decrypt any previously encrypted +data. You must proceed to Step 5 immediately. + +## Step 5: Create a New Encryption Key + +Generate a new encryption key tied to the new service account: + +```powershell +$configuration | New-SqlDscRSEncryptionKey -Force +``` + +This command calls the WMI method `ReencryptSecureInformation`, which: + +- Creates a new symmetric encryption key +- Associates the key with the new service account's security context +- Re-encrypts all stored secure information using the new key + +After this step, the Report Server can again encrypt and decrypt sensitive data. + +> [!TIP] +> After completing the entire service account change process, back up the new +> encryption key using `Backup-SqlDscRSEncryptionKey`. This backup is critical +> for disaster recovery scenarios. + +## Step 6: Recreate URL Reservations + +URL reservations in HTTP.sys are registered with the service account's Security +Identifier (SID). After changing accounts, you must recreate all URL reservations +to register them with the new account's SID: + + +```powershell +# Recreate all existing URL reservations +$configuration | Set-SqlDscRSUrlReservation -RecreateExisting -Force + +# Restart the service to apply URL reservation changes +$configuration | Restart-SqlDscRSService -Force +``` + + +The `-RecreateExisting` parameter instructs the command to: + +1. Retrieve all current URL reservations +1. Remove each reservation from HTTP.sys +1. Re-add each reservation (now registered to the new account's SID) + +This ensures the Report Server service can bind to its configured URLs. + +## Step 7: Re-Initialize the Report Server + +Re-initialize the Report Server to validate all configuration settings and ensure +the server is ready to handle requests: + +```powershell +# Re-initialize the instance +$configuration | Initialize-SqlDscRS -Force + +# Restart the service +$configuration | Restart-SqlDscRSService -Force +``` + +This calls the WMI method `InitializeReportServer`, which performs internal +validation and prepares the server for operation. + +## Step 8: Validate Accessibility + +Finally, verify that the Report Server is fully operational: + + +```powershell +# Refresh the configuration +$configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + +# Verify the service account +$finalServiceAccount = $configuration | Get-SqlDscRSServiceAccount +Write-Information -MessageData "Service account: $finalServiceAccount" -InformationAction 'Continue' + +# Verify initialization status +$isInitialized = $configuration | Test-SqlDscRSInitialized +Write-Information -MessageData "Is initialized: $isInitialized" -InformationAction 'Continue' + +# Verify URL reservations exist +$urlReservations = $configuration | Get-SqlDscRSUrlReservation +Write-Information -MessageData "URL reservations configured: $($urlReservations.Count)" -InformationAction 'Continue' + +# Test HTTP accessibility to all configured sites +$configuration | Test-SqlDscRSAccessible -Detailed -TimeoutSeconds 240 -RetryIntervalSeconds 10 +``` + + +The `Test-SqlDscRSAccessible` command tests HTTP connectivity to all configured +Report Server URLs and returns the accessibility status. The `-Detailed` parameter +provides verbose output about each URL tested. + +## Complete Script + +Here is the complete script combining all steps. Copy and customize for your +environment: + + +```powershell +#Requires -Modules SqlServerDsc +#Requires -RunAsAdministrator + +# ============================================================================ +# CONFIGURATION - Customize these values for your environment +# ============================================================================ + +$instanceName = 'SSRS' # Report Server instance name +$dbServerName = 'localhost' # SQL Server hosting RS databases +$dbInstanceName = 'RSDB' # SQL instance name (or 'MSSQLSERVER' for default) + +# ============================================================================ +# STEP 0: Get credentials and current configuration +# ============================================================================ + +$newCredential = Get-Credential -Message 'Enter the new service account credentials (DOMAIN\Username)' +$configuration = Get-SqlDscRSConfiguration -InstanceName $instanceName + +Write-Information -MessageData "Current service account: $($configuration.WindowsServiceIdentityActual)" -InformationAction 'Continue' + +# ============================================================================ +# STEP 1: Change the service account +# ============================================================================ + +Write-Information -MessageData "`n[Step 1] Changing service account..." -InformationAction 'Continue' + +$configuration | Set-SqlDscRSServiceAccount ` + -Credential $newCredential ` + -RestartService ` + -SuppressUrlReservationWarning ` + -Force + +# ============================================================================ +# STEP 2: Verify the change +# ============================================================================ + +Write-Information -MessageData "`n[Step 2] Verifying service account change..." -InformationAction 'Continue' + +$configuration = Get-SqlDscRSConfiguration -InstanceName $instanceName +$currentServiceAccount = $configuration | Get-SqlDscRSServiceAccount + +Write-Information -MessageData "Service account is now: $currentServiceAccount" -InformationAction 'Continue' + +# ============================================================================ +# STEP 3: Grant database permissions +# ============================================================================ + +Write-Information -MessageData "`n[Step 3] Granting database permissions..." -InformationAction 'Continue' + +# Create SQL login +$serverObject = Connect-SqlDscDatabaseEngine -ServerName $dbServerName -InstanceName $dbInstanceName +New-SqlDscLogin -ServerObject $serverObject -Name $currentServiceAccount -WindowsUser -Force +Disconnect-SqlDscDatabaseEngine -ServerObject $serverObject + +# Generate and execute database rights script +$databaseName = $configuration.DatabaseName +$databaseRightsScript = $configuration | Request-SqlDscRSDatabaseRightsScript ` + -DatabaseName $databaseName ` + -UserName $currentServiceAccount + +Invoke-SqlDscQuery ` + -ServerName $dbServerName ` + -InstanceName $dbInstanceName ` + -DatabaseName 'master' ` + -Query $databaseRightsScript ` + -Force + +# Restart service to apply permissions +$configuration | Restart-SqlDscRSService -Force + +Write-Information -MessageData "Database permissions granted" -InformationAction 'Continue' + +# ============================================================================ +# STEP 4: Remove old encryption key +# ============================================================================ + +Write-Information -MessageData "`n[Step 4] Removing old encryption key..." -InformationAction 'Continue' + +$configuration | Remove-SqlDscRSEncryptionKey -Force + +Write-Information -MessageData "Old encryption key removed" -InformationAction 'Continue' + +# ============================================================================ +# STEP 5: Create new encryption key +# ============================================================================ + +Write-Information -MessageData "`n[Step 5] Creating new encryption key..." -InformationAction 'Continue' + +$configuration | New-SqlDscRSEncryptionKey -Force + +Write-Information -MessageData "New encryption key created" -InformationAction 'Continue' + +# ============================================================================ +# STEP 6: Recreate URL reservations +# ============================================================================ + +Write-Information -MessageData "`n[Step 6] Recreating URL reservations..." -InformationAction 'Continue' + +$configuration | Set-SqlDscRSUrlReservation -RecreateExisting -Force +$configuration | Restart-SqlDscRSService -Force + +Write-Information -MessageData "URL reservations recreated" -InformationAction 'Continue' + +# ============================================================================ +# STEP 7: Re-initialize Report Server +# ============================================================================ + +Write-Information -MessageData "`n[Step 7] Re-initializing Report Server..." -InformationAction 'Continue' + +$configuration | Initialize-SqlDscRS -Force +$configuration | Restart-SqlDscRSService -Force + +Write-Information -MessageData "Report Server re-initialized" -InformationAction 'Continue' + +# ============================================================================ +# STEP 8: Validate accessibility +# ============================================================================ + +Write-Information -MessageData "`n[Step 8] Validating accessibility..." -InformationAction 'Continue' + +$configuration = Get-SqlDscRSConfiguration -InstanceName $instanceName + +$finalServiceAccount = $configuration | Get-SqlDscRSServiceAccount +$isInitialized = $configuration | Test-SqlDscRSInitialized +$urlReservations = $configuration | Get-SqlDscRSUrlReservation + +Write-Information -MessageData "`nFinal Status:" -InformationAction 'Continue' +Write-Information -MessageData " Service Account: $finalServiceAccount" -InformationAction 'Continue' +Write-Information -MessageData " Is Initialized: $isInitialized" -InformationAction 'Continue' +Write-Information -MessageData " URL Reservations: $($urlReservations.Count)" -InformationAction 'Continue' + +Write-Information -MessageData "`nTesting HTTP accessibility..." -InformationAction 'Continue' +$configuration | Test-SqlDscRSAccessible -Detailed -TimeoutSeconds 240 -RetryIntervalSeconds 10 + +Write-Information -MessageData "`n[Complete] Service account change finished successfully!" -InformationAction 'Continue' +``` + + +## Important Considerations + +### Plan for Downtime + +The Report Server will be unavailable during this process. Plan to perform this +change during a maintenance window when users do not need access to reports. + +### SQL Server 2017 Limitation + +On SQL Server 2017 Reporting Services, the encryption key operations (Steps 4-5) +may fail with the error: + +```text +The report server was unable to validate the integrity of encrypted data in the +database. (rsCannotValidateEncryptedData); Keyset does not exist +(Exception from HRESULT: 0x80090016) +``` + +This is a known limitation. The required steps to resolve this issue are unknown +at this time. If you encounter this error, you may need to use the Report Server +Configuration Manager to complete the encryption key steps manually. + +### Test in Non-Production First + +Always test this procedure in a non-production environment before applying it +to production servers. Verify that reports, subscriptions, and data sources +function correctly after the change. + +### Document Your Configuration + +Before making changes, document your current configuration including: + +- Current service account +- Database server and instance names +- URL reservations +- Any custom SSL certificate bindings + +This information is invaluable for troubleshooting if issues arise. + +## Summary + +Changing the service account for SQL Server Reporting Services or Power BI Report +Server requires a methodical approach that addresses encryption keys, database +permissions, and URL reservations. By following this eight-step process using +SqlServerDsc commands, you can automate this change reliably and consistently +across your Report Server infrastructure. + +The key takeaways are: + +1. The process must be performed in order — each step depends on the previous +1. Encryption key removal is destructive — there is no undo +1. Always validate accessibility after completing all steps +1. Plan for service downtime during the change diff --git a/source/WikiSource/Deploy-PowerBI-Report-Server.md b/source/WikiSource/Deploy-Power-BI-Report-Server.md similarity index 100% rename from source/WikiSource/Deploy-PowerBI-Report-Server.md rename to source/WikiSource/Deploy-Power-BI-Report-Server.md diff --git a/source/WikiSource/Setting-up-a-SQL-Server-AlwaysOn-Availability-Groups.md b/source/WikiSource/Setting-up-a-SQL-Server-AlwaysOn-Availability-Groups.md index bcc0c6b1e..b508d9e2c 100644 --- a/source/WikiSource/Setting-up-a-SQL-Server-AlwaysOn-Availability-Groups.md +++ b/source/WikiSource/Setting-up-a-SQL-Server-AlwaysOn-Availability-Groups.md @@ -1,5 +1,5 @@ --- -Category: Usage +Category: How-to --- # Setting up a SQL Server AlwaysOn Availability Groups diff --git a/source/WikiSource/Setting-up-a-SQL-Server-Failover-Cluster.md b/source/WikiSource/Setting-up-a-SQL-Server-Failover-Cluster.md index 577cacaa3..8393514bd 100644 --- a/source/WikiSource/Setting-up-a-SQL-Server-Failover-Cluster.md +++ b/source/WikiSource/Setting-up-a-SQL-Server-Failover-Cluster.md @@ -1,5 +1,5 @@ --- -Category: Usage +Category: How-to --- # Setting up a SQL Server Failover Cluster diff --git a/source/WikiSource/Troubleshooting-Report-Server.md b/source/WikiSource/Troubleshooting-Report-Server.md new file mode 100644 index 000000000..932d5b107 --- /dev/null +++ b/source/WikiSource/Troubleshooting-Report-Server.md @@ -0,0 +1,208 @@ +--- +Category: How-to +--- + +# Troubleshooting Report Server + +This guide covers diagnostic techniques for troubleshooting _Power BI Report Server_ +(PBIRS) and _SQL Server Reporting Services_ (SSRS). It explains how to retrieve +log file locations, analyze log content, and query Windows event logs for +error information. + +> [!NOTE] +> The examples in this guide use the instance name `'PBIRS'` for _Power BI +> Report Server_. If you are using _SQL Server Reporting Services_ or have a +> custom instance name, substitute `'PBIRS'` with your instance name (e.g., +> `'SSRS'` or your custom name). + +## Log Files + +Report servers store log files in the `ErrorDumpDirectory` configured during +setup. This folder contains service logs, portal logs, and memory dumps that +are useful for diagnosing issues. + +### Getting the Log Path + +Use the `Get-SqlDscRSLogPath` command to retrieve the log folder path: + +```powershell +Get-SqlDscRSLogPath -InstanceName 'PBIRS' +``` + +Alternatively, pipe a configuration object from `Get-SqlDscRSConfiguration`: + +```powershell +Get-SqlDscRSConfiguration -InstanceName 'PBIRS' | Get-SqlDscRSLogPath +``` + +### Common Log File Types + +The log folder typically contains these file types: + +- `ReportingServicesService*.log` - Web service activity and error logs +- `RSPortal*.log` - Portal access and activity logs +- `SQLDumpr*.mdmp` - Memory dumps for crash analysis + +### Listing Log Files + +To list all files in the log folder with their sizes and timestamps: + +```powershell +$logPath = Get-SqlDscRSLogPath -InstanceName 'PBIRS' + +Get-ChildItem -Path $logPath -Recurse -File | + Select-Object -Property FullName, Length, LastWriteTime +``` + +To filter for only `.log` files: + +```powershell +$logPath = Get-SqlDscRSLogPath -InstanceName 'PBIRS' + +Get-ChildItem -Path $logPath -Recurse -File -Filter '*.log' | + Select-Object -Property FullName, Length, LastWriteTime +``` + +### Reading Log Content + +To read the last 50 lines from the most recent service log: + + +```powershell +$logPath = Get-SqlDscRSLogPath -InstanceName 'PBIRS' + +Get-ChildItem -Path $logPath -Filter 'ReportingServicesService*.log' | + Sort-Object -Property LastWriteTime -Descending | + Select-Object -First 1 | + ForEach-Object -Process { Get-Content -Path $_.FullName -Tail 50 } +``` + + +To read from all log files: + +```powershell +$logPath = Get-SqlDscRSLogPath -InstanceName 'PBIRS' + +Get-ChildItem -Path $logPath -Filter '*.log' -Recurse | ForEach-Object -Process { + Write-Host "--- $($_.Name) ---" -ForegroundColor Cyan + Get-Content -Path $_.FullName -Tail 50 + Write-Host "--- End of $($_.Name) ---" -ForegroundColor Cyan +} +``` + +## Windows Event Log + +Report server components write events to the Windows Application log. Querying +these events can reveal errors not captured in the file-based logs. + +### Relevant Event Providers + +When filtering events, look for these provider names: + +- `Report Server Windows Service (SSRS)` - Core report server service events +- `RSInstallerEventLog` - Installation and configuration events +- `MSSQL$` - Database engine events (e.g., `MSSQL$PBIRS`) +- `SQLAgent$` - SQL Agent events if subscriptions are used + +### Querying Error Events + +To retrieve the last 50 error events from the Application log: + + +```powershell +Get-WinEvent -LogName 'Application' -MaxEvents 50 -FilterXPath '*[System[Level=2]]' | + ForEach-Object -Process { + "[$($_.TimeCreated)] [$($_.ProviderName)] $($_.Message)" + } +``` + + +To filter for report server-specific providers: + + +```powershell +$providers = @( + 'Report Server Windows Service (SSRS)' + 'RSInstallerEventLog' +) + +Get-WinEvent -LogName 'Application' -MaxEvents 100 | + Where-Object -FilterScript { $_.ProviderName -in $providers } | + ForEach-Object -Process { + "[$($_.TimeCreated)] [$($_.LevelDisplayName)] $($_.Message)" + } +``` + + +### Listing Active Providers + +To see which providers have written recent events (useful for identifying +the correct provider name for your environment): + +```powershell +Get-WinEvent -LogName 'Application' -MaxEvents 200 | + Select-Object -ExpandProperty ProviderName -Unique | + Sort-Object +``` + +## Complete Diagnostic Script + +The following script combines all diagnostic techniques into a single output +for troubleshooting: + + +```powershell +$instanceName = 'PBIRS' + +# Get log path +$logPath = Get-SqlDscRSLogPath -InstanceName $instanceName -ErrorAction 'Stop' + +Write-Host "Log path: $logPath" -ForegroundColor Green + +# List all files in log folder +if (Test-Path -Path $logPath) +{ + $allFiles = Get-ChildItem -Path $logPath -Recurse -File -ErrorAction 'SilentlyContinue' + + Write-Host "`nFiles in log folder ($($allFiles.Count) files):" -ForegroundColor Cyan + + foreach ($file in $allFiles) + { + Write-Host " $($file.FullName) (Size: $($file.Length) bytes, Modified: $($file.LastWriteTime))" + } + + # Output last 50 lines of each .log file + $logFiles = $allFiles | Where-Object -FilterScript { $_.Extension -eq '.log' } + + foreach ($logFile in $logFiles) + { + Write-Host "`n--- Last 50 lines of $($logFile.Name) ---" -ForegroundColor Yellow + + Get-Content -Path $logFile.FullName -Tail 50 -ErrorAction 'SilentlyContinue' + + Write-Host "--- End of $($logFile.Name) ---" -ForegroundColor Yellow + } +} +else +{ + Write-Warning -Message "Log path does not exist: $logPath" +} + +# Output Windows Application log errors +Write-Host "`n--- Last 50 Application log error events ---" -ForegroundColor Magenta + +$events = Get-WinEvent -LogName 'Application' -MaxEvents 50 -FilterXPath '*[System[Level=2]]' -ErrorAction 'SilentlyContinue' + +foreach ($event in $events) +{ + Write-Host "[$($event.TimeCreated)] [$($event.ProviderName)] $($event.Message)" +} + +Write-Host "--- End of Application log error events ---" -ForegroundColor Magenta +``` + + +> [!TIP] +> Save this script to a file and run it when troubleshooting report server +> issues. Redirect output to a file using `> diagnostic-output.txt` to share +> with support teams. diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 554f84b1b..4a6910c91 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -296,6 +296,13 @@ ConvertFrom-StringData @' 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-SqlDscRSLogPath + # cSpell: ignore GSRSLP + Get_SqlDscRSLogPath_GettingPath = Getting log file path for Reporting Services instance '{0}'. + Get_SqlDscRSLogPath_FoundPath = Found log file path: '{0}'. + Get_SqlDscRSLogPath_InstanceNotFound = Could not find a Reporting Services instance with the name '{0}'. (GSRSLP0001) + Get_SqlDscRSLogPath_LogPathNotFound = Could not determine the log file path for Reporting Services instance '{0}'. The ErrorDumpDirectory registry value is empty. (GSRSLP0002) + ## Get-SqlDscRSWebPortalApplicationName Get_SqlDscRSWebPortalApplicationName_GettingApplicationName = Getting web portal application name. @@ -761,6 +768,12 @@ ConvertFrom-StringData @' Set_SqlDscRSUrlReservation_DesiredUrls = Desired URL reservations for application '{0}': {1} Set_SqlDscRSUrlReservation_RemovingUrl = Removing URL reservation '{0}' for application '{1}' on Reporting Services instance '{2}'. Set_SqlDscRSUrlReservation_AddingUrl = Adding URL reservation '{0}' for application '{1}' on Reporting Services instance '{2}'. + Set_SqlDscRSUrlReservation_RecreatingUrl = Recreating URL reservation '{0}' for application '{1}' on Reporting Services instance '{2}'. (SSRUR0001) + Set_SqlDscRSUrlReservation_Recreate_ShouldProcessVerboseDescription = Recreating all URL reservations on Reporting Services instance '{0}'. + Set_SqlDscRSUrlReservation_Recreate_ShouldProcessVerboseWarning = Are you sure you want to recreate all URL reservations on Reporting Services instance '{0}'? All existing reservations will be removed and re-added. + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Set_SqlDscRSUrlReservation_Recreate_ShouldProcessCaption = Recreate URL reservations for Reporting Services instance + Set_SqlDscRSUrlReservation_NoReservationsToRecreate = No URL reservations found to recreate on Reporting Services instance '{0}'. ## Set-SqlDscRSVirtualDirectory Set_SqlDscRSVirtualDirectory_Setting = Setting virtual directory '{0}' for application '{1}' on Reporting Services instance '{2}'. @@ -778,6 +791,7 @@ ConvertFrom-StringData @' ## Request-SqlDscRSDatabaseRightsScript Request_SqlDscRSDatabaseRightsScript_Generating = Generating database rights script for database '{0}' and user '{1}' for Reporting Services instance '{2}'. Request_SqlDscRSDatabaseRightsScript_FailedToGenerate = Failed to generate database rights script for Reporting Services instance '{0}'. {1} (RSRDBRS0001) + Request_SqlDscRSDatabaseRightsScript_InvalidUserNameFormat = When using IsRemote with Windows authentication, the UserName must be in the format '\'. The provided value '{0}' is not in the correct format. ## Set-SqlDscRSDatabaseConnection Set_SqlDscRSDatabaseConnection_Setting = Setting report server database connection to database '{0}' on server '{1}' for Reporting Services instance '{2}'. @@ -798,6 +812,44 @@ ConvertFrom-StringData @' Restart_SqlDscRSService_WaitingBeforeStart = Waiting {0} seconds before starting service '{1}'. Restart_SqlDscRSService_StartingDependentService = Starting dependent service '{0}'. + ## New-SqlDscRSEncryptionKey + New_SqlDscRSEncryptionKey_Generating = Generating new encryption key for Reporting Services instance '{0}'. + New_SqlDscRSEncryptionKey_ShouldProcessDescription = Generating new encryption key for Reporting Services instance '{0}'. This will invalidate existing encryption key backups. + New_SqlDscRSEncryptionKey_ShouldProcessConfirmation = Are you sure you want to generate a new encryption key for Reporting Services instance '{0}'? This will invalidate existing backups. + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + New_SqlDscRSEncryptionKey_ShouldProcessCaption = Generate new encryption key for Reporting Services instance + New_SqlDscRSEncryptionKey_FailedToGenerate = Failed to generate new encryption key for Reporting Services instance '{0}'. {1} (NSRSEK0001) + New_SqlDscRSEncryptionKey_BackupReminder = A new encryption key has been generated. Previous encryption key backups are no longer valid. Back up the new encryption key immediately using Backup-SqlDscRSEncryptionKey. + + ## Remove-SqlDscRSEncryptionKey + Remove_SqlDscRSEncryptionKey_Removing = Removing encryption key from Reporting Services instance '{0}'. + Remove_SqlDscRSEncryptionKey_ShouldProcessDescription = Removing encryption key from Reporting Services instance '{0}'. This is a destructive operation. + Remove_SqlDscRSEncryptionKey_ShouldProcessConfirmation = Are you sure you want to remove the encryption key from Reporting Services instance '{0}'? This is a destructive operation. + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Remove_SqlDscRSEncryptionKey_ShouldProcessCaption = Remove encryption key from Reporting Services instance + Remove_SqlDscRSEncryptionKey_FailedToRemove = Failed to remove encryption key from Reporting Services instance '{0}'. (RRSEK0001) + + ## Remove-SqlDscRSEncryptedInformation + Remove_SqlDscRSEncryptedInformation_Removing = Removing encrypted information from Reporting Services instance '{0}'. + Remove_SqlDscRSEncryptedInformation_ShouldProcessDescription = Removing encrypted information from Reporting Services instance '{0}'. This is a destructive operation. + Remove_SqlDscRSEncryptedInformation_ShouldProcessConfirmation = Are you sure you want to remove all encrypted information from Reporting Services instance '{0}'? This is a destructive operation. + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Remove_SqlDscRSEncryptedInformation_ShouldProcessCaption = Remove encrypted information from Reporting Services instance + Remove_SqlDscRSEncryptedInformation_FailedToRemove = Failed to remove encrypted information from Reporting Services instance '{0}'. (RRSREI0001) + + ## Get-SqlDscRSServiceAccount + Get_SqlDscRSServiceAccount_Getting = Getting service account for Reporting Services instance '{0}'. + + ## Set-SqlDscRSServiceAccount + Set_SqlDscRSServiceAccount_Setting = Setting service account to '{0}' for Reporting Services instance '{1}'. + Set_SqlDscRSServiceAccount_ShouldProcessDescription = Setting service account to '{0}' for Reporting Services instance '{1}'. + Set_SqlDscRSServiceAccount_ShouldProcessConfirmation = Are you sure you want to set the service account to '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Set_SqlDscRSServiceAccount_ShouldProcessCaption = Set service account for Reporting Services instance + Set_SqlDscRSServiceAccount_FailedToSet = Failed to set service account for Reporting Services instance '{0}'. {1} (SSRSSA0001) + Set_SqlDscRSServiceAccount_RestartingService = Restarting Reporting Services service '{0}'. + Set_SqlDscRSServiceAccount_UrlReservationWarning = The service account has been changed from '{0}' to '{1}'. URL reservations are tied to the service account and must be recreated. To recreate URL reservations for the new service account, run: `Get-SqlDscRSConfiguration -InstanceName '' | Set-SqlDscRSUrlReservation -RecreateExisting -Force` (SSRSSA0002) + ## Test-SqlDscRSInitialized Test_SqlDscRSInitialized_Testing = Testing if Reporting Services instance '{0}' is initialized. Test_SqlDscRSInitialized_IsInitialized = Reporting Services instance '{0}' is initialized. diff --git a/tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1 new file mode 100644 index 000000000..fc30e7472 --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1 @@ -0,0 +1,93 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +Describe 'Get-SqlDscRSLogPath' { + Context 'When getting the log path for SQL Server Reporting Services instance' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS') { + It 'Should return the correct log path for SSRS instance' { + $result = Get-SqlDscRSLogPath -InstanceName 'SSRS' -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -Be 'C:\Program Files\SSRS\SSRS\LogFiles' + Test-Path -Path $result | Should -BeTrue + } + + It 'Should return a path that contains log files' { + $logPath = Get-SqlDscRSLogPath -InstanceName 'SSRS' -ErrorAction 'Stop' + + $logFiles = Get-ChildItem -Path $logPath -Filter '*.log' -ErrorAction 'Stop' + + # After initialization, there should be log files + $logFiles | Should -Not -BeNullOrEmpty + } + + It 'Should work with pipeline input from Get-SqlDscRSConfiguration' { + $result = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' | Get-SqlDscRSLogPath -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -Be 'C:\Program Files\SSRS\SSRS\LogFiles' + Test-Path -Path $result | Should -BeTrue + } + } + + Context 'When getting the log path for Power BI Report Server instance' -Tag @('Integration_PowerBI') { + # cSpell: ignore PBIRS + It 'Should return the correct log path for PBIRS instance' { + $result = Get-SqlDscRSLogPath -InstanceName 'PBIRS' -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -Be 'C:\Program Files\PBIRS\PBIRS\LogFiles' + Test-Path -Path $result | Should -BeTrue + } + + It 'Should return a path that contains log files' { + $logPath = Get-SqlDscRSLogPath -InstanceName 'PBIRS' -ErrorAction 'Stop' + + $logFiles = Get-ChildItem -Path $logPath -Filter '*.log' -ErrorAction 'Stop' + + # After initialization, there should be log files + $logFiles | Should -Not -BeNullOrEmpty + } + + It 'Should work with pipeline input from Get-SqlDscRSConfiguration' { + $result = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' | Get-SqlDscRSLogPath -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -Be 'C:\Program Files\PBIRS\PBIRS\LogFiles' + Test-Path -Path $result | Should -BeTrue + } + } + + Context 'When trying to get the log path for a non-existent instance' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + It 'Should throw a terminating error' { + { Get-SqlDscRSLogPath -InstanceName 'NonExistentInstance' -ErrorAction 'Stop' } | Should -Throw + } + } +} diff --git a/tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1 new file mode 100644 index 000000000..8a0a5f3c3 --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1 @@ -0,0 +1,96 @@ +[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 verify that the service account can be retrieved for Reporting + Services instances. The tests run after Set-SqlDscRSServiceAccount has + changed the service account to svc-RS, so we verify that account is set. +#> +Describe 'Get-SqlDscRSServiceAccount' { + Context 'When getting service account for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $computerName + } + + It 'Should return the expected service account' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:expectedServiceAccount + } + } + + Context 'When getting service account for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $computerName + } + + It 'Should return the expected service account' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:expectedServiceAccount + } + } + + Context 'When getting service account for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $computerName + } + + It 'Should return the expected service account' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:expectedServiceAccount + } + } + + Context 'When getting service account for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $computerName + } + + It 'Should return the expected service account' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:expectedServiceAccount + } + } +} diff --git a/tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1 b/tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1 new file mode 100644 index 000000000..bb44fb38c --- /dev/null +++ b/tests/Integration/Commands/New-SqlDscRSEncryptionKey.Integration.Tests.ps1 @@ -0,0 +1,97 @@ +[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 + This test is marked as potentially destructive because it regenerates the + encryption key which will invalidate all existing encrypted data. + Use with caution in test environments only. +#> + +<# + TODO: The following integration tests are skipped on SQL Server 2017 due to + encryption key validation failures. These tests are linked and all fail + with similar errors related to "rsCannotValidateEncryptedData" and + "Keyset does not exist". + + Failing tests on SQL Server 2017: + - Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - New-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - Post.Reinitialize.RS.Integration.Tests.ps1 + - Post.ServiceAccountChange.RS.Integration.Tests.ps1 + + Error: "The report server was unable to validate the integrity of encrypted + data in the database. (rsCannotValidateEncryptedData);Keyset does not exist + (Exception from HRESULT: 0x80090016)" +#> +Describe 'New-SqlDscRSEncryptionKey' { + Context 'When creating a new encryption key for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') -Skip { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should create a new encryption key' { + $null = $script:configuration | New-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } + + Context 'When creating a new encryption key for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should create a new encryption key' { + $null = $script:configuration | New-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } + + Context 'When creating a new encryption key for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should create a new encryption key' { + $null = $script:configuration | New-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } + + Context 'When creating a new encryption key for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + } + + It 'Should create a new encryption key' { + $null = $script:configuration | New-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..709c0e758 --- /dev/null +++ b/tests/Integration/Commands/Post.DatabaseRights.RS.Integration.Tests.ps1 @@ -0,0 +1,121 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # 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 + This test file grants database rights to the new service account after + the service account has been changed. The new service account needs + database permissions to access the ReportServer and ReportServerTempDB + databases. + + This test runs after Set-SqlDscRSServiceAccount and Get-SqlDscRSServiceAccount + tests, and before Post.UrlReservationRecreate.RS to ensure the service + account has database access before testing accessibility. +#> +Describe 'Post.DatabaseRights.RS' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + BeforeAll { + if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') + { + $script:instanceName = 'PBIRS' + } + else + { + # Default to SSRS for SQL2017_RS, SQL2019_RS, SQL2022_RS + $script:instanceName = 'SSRS' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + # Get database name from configuration + $script:databaseName = $script:configuration.DatabaseName + + Write-Verbose -Message "Instance: $script:instanceName, Database: $script:databaseName, ServiceAccount: $script:serviceAccount" -Verbose + } + + Context 'When granting database rights to the new service account' { + It 'Should create a SQL Server login for the new service account' { + # Use the RSDB instance that hosts the ReportServer database + $serverObject = Connect-SqlDscDatabaseEngine -ServerName 'localhost' -InstanceName 'RSDB' -ErrorAction 'Stop' + + try + { + # The login must exist before granting database rights + $null = New-SqlDscLogin -ServerObject $serverObject -Name $script:serviceAccount -WindowsUser -Force -ErrorAction 'Stop' + } + finally + { + Disconnect-SqlDscDatabaseEngine -ServerObject $serverObject -ErrorAction 'SilentlyContinue' + } + } + + It 'Should generate database rights script for the new service account' { + # When IsWindowsUser is set to false, the SQL Server user must already exist on the SQL Server for the script to run successfully. + $script:databaseRightsScript = $script:configuration | + Request-SqlDscRSDatabaseRightsScript -DatabaseName $script:databaseName -UserName $script:serviceAccount -ErrorAction 'Stop' + + $script:databaseRightsScript | Should -Not -BeNullOrEmpty -Because 'the database rights script should be generated' + } + + It 'Should execute the database rights script against the database' { + # Use the RSDB instance that hosts the ReportServer database + $serverObject = Connect-SqlDscDatabaseEngine -ServerName 'localhost' -InstanceName 'RSDB' -ErrorAction 'Stop' + + try + { + $invokeSqlDscQueryParameters = @{ + ServerName = 'localhost' + InstanceName = 'RSDB' + DatabaseName = 'master' + Query = $script:databaseRightsScript + Force = $true + ErrorAction = 'Stop' + } + + Invoke-SqlDscQuery @invokeSqlDscQueryParameters + } + finally + { + Disconnect-SqlDscDatabaseEngine -ServerObject $serverObject -ErrorAction 'SilentlyContinue' + } + } + + It 'Should restart the Reporting Services service to apply changes' { + $null = $script:configuration | Restart-SqlDscRSService -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Post.EncryptedInformation.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.EncryptedInformation.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..9c16677c0 --- /dev/null +++ b/tests/Integration/Commands/Post.EncryptedInformation.RS.Integration.Tests.ps1 @@ -0,0 +1,58 @@ +[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 + This test is specifically for SQL Server 2017 where the encryption key + operations (Remove-SqlDscRSEncryptionKey and New-SqlDscRSEncryptionKey) + fail with "rsCannotValidateEncryptedData" and "Keyset does not exist" + errors. + + As a workaround for SQL Server 2017, this test removes all encrypted + information from the report server database using + Remove-SqlDscRSEncryptedInformation. + + This test runs after Post.DatabaseRights.RS.Integration.Tests.ps1 and + before New-SqlDscRSEncryptionKey.Integration.Tests.ps1. +#> +Describe 'Post.EncryptedInformation.RS' { + Context 'When removing encrypted information for SQL Server Reporting Services on SQL Server 2017' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encrypted information' { + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..1758a4def --- /dev/null +++ b/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 @@ -0,0 +1,106 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # 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 + This test file re-initializes Reporting Services after the service account + has been changed and URL reservations have been recreated. This ensures + that the Reporting Services instance is fully functional with the new + service account. + + This test runs after Post.UrlReservationRecreate.RS and before + Post.ServiceAccountChange.RS to ensure the instance is properly + initialized before testing accessibility. +#> + +<# + TODO: The following integration tests are skipped on SQL Server 2017 due to + encryption key validation failures. These tests are linked and all fail + with similar errors related to "rsCannotValidateEncryptedData" and + "Keyset does not exist". + + Failing tests on SQL Server 2017: + - Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - New-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - Post.Reinitialize.RS.Integration.Tests.ps1 + - Post.ServiceAccountChange.RS.Integration.Tests.ps1 + + Error: "The report server was unable to validate the integrity of encrypted + data in the database. (rsCannotValidateEncryptedData);Keyset does not exist + (Exception from HRESULT: 0x80090016)" + + Re-add tag 'Integration_SQL2017_RS' when fixed. +#> +Describe 'Post.Reinitialize.RS' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + BeforeAll { + if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') + { + $script:instanceName = 'PBIRS' + } + else + { + # Default to SSRS for SQL2017_RS, SQL2019_RS, SQL2022_RS + $script:instanceName = 'SSRS' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + Write-Verbose -Message "Instance: $script:instanceName, ServiceAccount: $script:serviceAccount" -Verbose + } + + Context 'When re-initializing Reporting Services after service account change' { + It 'Should re-initialize the Reporting Services instance' { + $script:configuration | Initialize-SqlDscRS -Force -ErrorAction 'Stop' + } + + It 'Should have an initialized instance after re-initialization' { + # Refresh the configuration after initialization + $configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + $isInitialized = $configuration | Test-SqlDscRSInitialized -ErrorAction 'Stop' + + $isInitialized | Should -BeTrue -Because 'the instance should be initialized after re-initialization' + + Write-Verbose -Message "Instance initialized: $isInitialized" -Verbose + } + + It 'Should restart the Reporting Services service' { + $null = $script:configuration | Restart-SqlDscRSService -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..942e6246f --- /dev/null +++ b/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 @@ -0,0 +1,129 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # 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 + This test file validates that Reporting Services sites are accessible + after the service account has been changed. It runs after the + Set-SqlDscRSServiceAccount and Get-SqlDscRSServiceAccount tests to + verify the RS configuration remains functional after a service account + change. + + Uses URL reservations from the configuration CIM instance via the + Configuration parameter set of Test-SqlDscRSAccessible. +#> + +<# + TODO: The following integration tests are skipped on SQL Server 2017 due to + encryption key validation failures. These tests are linked and all fail + with similar errors related to "rsCannotValidateEncryptedData" and + "Keyset does not exist". + + Failing tests on SQL Server 2017: + - Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - New-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - Post.Reinitialize.RS.Integration.Tests.ps1 + - Post.ServiceAccountChange.RS.Integration.Tests.ps1 + + Error: "The report server was unable to validate the integrity of encrypted + data in the database. (rsCannotValidateEncryptedData);Keyset does not exist + (Exception from HRESULT: 0x80090016)" + + Re-add tag 'Integration_SQL2017_RS' when fixed. +#> +Describe 'Post.ServiceAccountChange.RS' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + BeforeAll { + if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') + { + $script:instanceName = 'PBIRS' + } + else + { + # Default to SSRS for SQL2017_RS, SQL2019_RS, SQL2022_RS + $script:instanceName = 'SSRS' + } + + $computerName = Get-ComputerName + $script:expectedServiceAccount = '{0}\svc-RS' -f $computerName + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + # Get expected URL reservations + $script:urlReservations = $script:configuration | Get-SqlDscRSUrlReservation -ErrorAction 'Stop' + + Write-Verbose -Message "Instance: $script:instanceName, URL Reservations: Application=$($script:urlReservations.Application -join ',') UrlString=$($script:urlReservations.UrlString -join ',')" -Verbose + } + + Context 'When validating Reporting Services accessibility after service account change' { + It 'Should have the expected service account set' { + $currentServiceAccount = $script:configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $currentServiceAccount | Should -BeExactly $script:expectedServiceAccount -Because 'the service account should have been changed by Set-SqlDscRSServiceAccount tests' + } + + It 'Should have an initialized instance' { + $isInitialized = $script:configuration | Test-SqlDscRSInitialized -ErrorAction 'Stop' + + $isInitialized | Should -BeTrue -Because 'the instance should remain initialized after service account change' + } + + It 'Should have URL reservations configured' { + $script:urlReservations | Should -Not -BeNullOrEmpty + $script:urlReservations.Application | Should -Not -BeNullOrEmpty -Because 'URL reservations should have applications configured' + $script:urlReservations.UrlString | Should -Not -BeNullOrEmpty -Because 'URL reservations should have URL strings configured' + } + + It 'Should have all configured sites accessible after service account change' { + $results = $script:configuration | Test-SqlDscRSAccessible -Detailed -TimeoutSeconds 240 -RetryIntervalSeconds 10 -ErrorAction 'Stop' -Verbose + + Write-Verbose -Message "Accessibility results: $($results | ConvertTo-Json -Compress)" -Verbose + + # Verify we got results for the expected applications + $expectedApplications = $script:urlReservations.Application | Select-Object -Unique + + $results | Should -Not -BeNullOrEmpty -Because 'the command should return site accessibility results' + $results | Should -HaveCount $expectedApplications.Count -Because "we expect results for each unique application ($($expectedApplications -join ', '))" + + foreach ($application in $expectedApplications) + { + $siteResult = $results | Where-Object -FilterScript { $_.Site -eq $application } + + $siteResult | Should -Not -BeNullOrEmpty -Because "the '$application' site should have a result" + $siteResult.Accessible | Should -BeTrue -Because "the '$application' site should be accessible after service account change" + $siteResult.StatusCode | Should -Be 200 -Because "the '$application' site should return HTTP 200 after service account change" + } + } + } +} diff --git a/tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..a8f2e41f3 --- /dev/null +++ b/tests/Integration/Commands/Post.UrlReservationRecreate.RS.Integration.Tests.ps1 @@ -0,0 +1,111 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../../TestHelpers/CommonTestHelper.psm1') + + # 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 + This test file recreates all URL reservations after the service account + has been changed. URL reservations are tied to the Windows service account + and must be recreated after changing the account to use the new account's + security context. + + This test runs after Post.ServiceAccountChange.RS to ensure the service + account has been verified before recreating URL reservations. +#> +Describe 'Post.UrlReservationRecreate.RS' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + BeforeAll { + if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') + { + $script:instanceName = 'PBIRS' + } + else + { + # Default to SSRS for SQL2017_RS, SQL2019_RS, SQL2022_RS + $script:instanceName = 'SSRS' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName $script:instanceName -ErrorAction 'Stop' + + # Get URL reservations before recreating + $script:urlReservationsBefore = $script:configuration | Get-SqlDscRSUrlReservation -ErrorAction 'Stop' + + Write-Verbose -Message "Instance: $script:instanceName, URL Reservations before: Application=$($script:urlReservationsBefore.Application -join ',') UrlString=$($script:urlReservationsBefore.UrlString -join ',')" -Verbose + } + + Context 'When recreating URL reservations after service account change' { + It 'Should have URL reservations to recreate' { + $script:urlReservationsBefore | Should -Not -BeNullOrEmpty + $script:urlReservationsBefore.Application | Should -Not -BeNullOrEmpty -Because 'URL reservations should have applications configured' + $script:urlReservationsBefore.UrlString | Should -Not -BeNullOrEmpty -Because 'URL reservations should have URL strings configured' + } + + It 'Should recreate all URL reservations without throwing' { + $null = $script:configuration | Set-SqlDscRSUrlReservation -RecreateExisting -Force -ErrorAction 'Stop' + } + + It 'Should restart the Reporting Services service' { + $null = $script:configuration | Restart-SqlDscRSService -Force -ErrorAction 'Stop' + } + + It 'Should have the same URL reservations after recreating' { + $urlReservationsAfter = $script:configuration | Get-SqlDscRSUrlReservation -ErrorAction 'Stop' + + Write-Verbose -Message "URL Reservations after: Application=$($urlReservationsAfter.Application -join ',') UrlString=$($urlReservationsAfter.UrlString -join ',')" -Verbose + + $urlReservationsAfter.Application.Count | Should -Be $script:urlReservationsBefore.Application.Count -Because 'the number of URL reservations should remain the same' + + # Verify each application and URL combination exists + for ($i = 0; $i -lt $script:urlReservationsBefore.Application.Count; $i++) + { + $expectedApplication = $script:urlReservationsBefore.Application[$i] + $expectedUrl = $script:urlReservationsBefore.UrlString[$i] + + # Find matching reservation in after list + $found = $false + + for ($j = 0; $j -lt $urlReservationsAfter.Application.Count; $j++) + { + if ($urlReservationsAfter.Application[$j] -eq $expectedApplication -and $urlReservationsAfter.UrlString[$j] -eq $expectedUrl) + { + $found = $true + + break + } + } + + $found | Should -BeTrue -Because "URL reservation '$expectedUrl' for application '$expectedApplication' should exist after recreation" + } + } + } +} diff --git a/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 b/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 index f513e449e..8d9bc7401 100644 --- a/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 @@ -92,6 +92,13 @@ Describe 'Prerequisites' { $user.Name | Should -Be 'svc-SqlAgentSec' (Get-LocalUser -Name 'svc-SqlAgentSec').Name | Should -Be 'svc-SqlAgentSec' } + + It 'Should create svc-RS user' -Tag @('Integration_PowerBI', 'Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { + $user = New-LocalUser -Name 'svc-RS' -Password $password -FullName 'svc-RS' -Description 'Runs the Reporting Services service.' + + $user.Name | Should -Be 'svc-RS' + (Get-LocalUser -Name 'svc-RS').Name | Should -Be 'svc-RS' + } } Context 'Create required local Windows groups' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI', 'Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { diff --git a/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 new file mode 100644 index 000000000..7593c2710 --- /dev/null +++ b/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 @@ -0,0 +1,81 @@ +[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 + This test is marked as potentially destructive because it removes all + encrypted information stored in the report server database. + Use with caution in test environments only. +#> + +Describe 'Remove-SqlDscRSEncryptedInformation' { + Context 'When removing encrypted information for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') -Skip:$true { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encrypted information' { + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + } + } + + Context 'When removing encrypted information for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encrypted information' { + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + } + } + + Context 'When removing encrypted information for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encrypted information' { + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + } + } + + Context 'When removing encrypted information for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + } + + It 'Should remove the encrypted information' { + $null = $script:configuration | Remove-SqlDscRSEncryptedInformation -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 new file mode 100644 index 000000000..6794d666c --- /dev/null +++ b/tests/Integration/Commands/Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 @@ -0,0 +1,98 @@ +[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 + This test is marked as potentially destructive because it removes the + encryption key which will delete all encrypted data. + Use with caution in test environments only. +#> + +<# + TODO: The following integration tests are skipped on SQL Server 2017 due to + encryption key validation failures. These tests are linked and all fail + with similar errors related to "rsCannotValidateEncryptedData" and + "Keyset does not exist". + + Failing tests on SQL Server 2017: + - Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - New-SqlDscRSEncryptionKey.Integration.Tests.ps1 + - Post.Reinitialize.RS.Integration.Tests.ps1 + - Post.ServiceAccountChange.RS.Integration.Tests.ps1 + + Error: "The report server was unable to validate the integrity of encrypted + data in the database. (rsCannotValidateEncryptedData);Keyset does not exist + (Exception from HRESULT: 0x80090016)" +#> +Describe 'Remove-SqlDscRSEncryptionKey' { + Context 'When removing encryption key for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') -Skip:$true { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encryption key' { + # Tried using -IncludeEncryptedInformation but it still fails on SQL Server 2017, see TODO above. + $null = $script:configuration | Remove-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } + + Context 'When removing encryption key for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encryption key' { + $null = $script:configuration | Remove-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } + + Context 'When removing encryption key for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should remove the encryption key' { + $null = $script:configuration | Remove-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } + + Context 'When removing encryption key for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + } + + It 'Should remove the encryption key' { + $null = $script:configuration | Remove-SqlDscRSEncryptionKey -Force -ErrorAction 'Stop' + } + } +} diff --git a/tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1 b/tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1 new file mode 100644 index 000000000..0890573e7 --- /dev/null +++ b/tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1 @@ -0,0 +1,168 @@ +[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 change the Reporting Services service account to the svc-RS + account that was created in the Prerequisites tests. The Get-SqlDscRSServiceAccount + tests run after this and verify the account was changed successfully. +#> +Describe 'Set-SqlDscRSServiceAccount' { + Context 'When setting service account for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $computerName = Get-ComputerName + $script:serviceAccountName = '{0}\svc-RS' -f $computerName + $script:serviceAccountPassword = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force + $script:serviceAccountCredential = [System.Management.Automation.PSCredential]::new($script:serviceAccountName, $script:serviceAccountPassword) + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $script:originalServiceAccount = $script:configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + } + + It 'Should have the original service account set' { + $script:originalServiceAccount | Should -Not -BeNullOrEmpty + + Write-Verbose -Message "Original service account: $script:originalServiceAccount" -Verbose + } + + It 'Should change the service account to svc-RS' { + $script:configuration | + Set-SqlDscRSServiceAccount -Credential $script:serviceAccountCredential -RestartService -SuppressUrlReservationWarning -Force -ErrorAction 'Stop' + } + + It 'Should have the new service account set after the change' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:serviceAccountName + + Write-Verbose -Message "New service account after change: $result" -Verbose + } + } + + Context 'When setting service account for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $computerName = Get-ComputerName + $script:serviceAccountName = '{0}\svc-RS' -f $computerName + $script:serviceAccountPassword = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force + $script:serviceAccountCredential = [System.Management.Automation.PSCredential]::new($script:serviceAccountName, $script:serviceAccountPassword) + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $script:originalServiceAccount = $script:configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + } + + It 'Should have the original service account set' { + $script:originalServiceAccount | Should -Not -BeNullOrEmpty + + Write-Verbose -Message "Original service account: $script:originalServiceAccount" -Verbose + } + + It 'Should change the service account to svc-RS' { + $script:configuration | + Set-SqlDscRSServiceAccount -Credential $script:serviceAccountCredential -RestartService -SuppressUrlReservationWarning -Force -ErrorAction 'Stop' + } + + It 'Should have the new service account set after the change' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:serviceAccountName + + Write-Verbose -Message "New service account after change: $result" -Verbose + } + } + + Context 'When setting service account for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $computerName = Get-ComputerName + $script:serviceAccountName = '{0}\svc-RS' -f $computerName + $script:serviceAccountPassword = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force + $script:serviceAccountCredential = [System.Management.Automation.PSCredential]::new($script:serviceAccountName, $script:serviceAccountPassword) + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $script:originalServiceAccount = $script:configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + } + + It 'Should have the original service account set' { + $script:originalServiceAccount | Should -Not -BeNullOrEmpty + + Write-Verbose -Message "Original service account: $script:originalServiceAccount" -Verbose + } + + It 'Should change the service account to svc-RS' { + $script:configuration | + Set-SqlDscRSServiceAccount -Credential $script:serviceAccountCredential -RestartService -SuppressUrlReservationWarning -Force -ErrorAction 'Stop' + } + + It 'Should have the new service account set after the change' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:serviceAccountName + + Write-Verbose -Message "New service account after change: $result" -Verbose + } + } + + Context 'When setting service account for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $computerName = Get-ComputerName + $script:serviceAccountName = '{0}\svc-RS' -f $computerName + $script:serviceAccountPassword = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force + $script:serviceAccountCredential = [System.Management.Automation.PSCredential]::new($script:serviceAccountName, $script:serviceAccountPassword) + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + $script:originalServiceAccount = $script:configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + } + + It 'Should have the original service account set' { + $script:originalServiceAccount | Should -Not -BeNullOrEmpty + + Write-Verbose -Message "Original service account: $script:originalServiceAccount" -Verbose + } + + It 'Should change the service account to svc-RS' { + $script:configuration | + Set-SqlDscRSServiceAccount -Credential $script:serviceAccountCredential -RestartService -SuppressUrlReservationWarning -Force -ErrorAction 'Stop' + } + + It 'Should have the new service account set after the change' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSServiceAccount -ErrorAction 'Stop' + + $result | Should -BeExactly $script:serviceAccountName + + Write-Verbose -Message "New service account after change: $result" -Verbose + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSLogPath.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSLogPath.Tests.ps1 new file mode 100644 index 000000000..a955fa108 --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSLogPath.Tests.ps1 @@ -0,0 +1,205 @@ +[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 'Get-SqlDscRSLogPath' { + Context 'When parameter validation' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'ByInstanceName' + ExpectedParameters = '-InstanceName []' + } + @{ + ExpectedParameterSetName = 'ByConfiguration' + ExpectedParameters = '-Configuration []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSLogPath').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 InstanceName as a mandatory parameter in the ByInstanceName parameter set' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSLogPath').Parameters['InstanceName'] + + $parameterInfo.Attributes.Where({$_.ParameterSetName -eq 'ByInstanceName'}).Mandatory | Should -BeTrue + } + + It 'Should have Configuration as a mandatory parameter in the ByConfiguration parameter set' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSLogPath').Parameters['Configuration'] + + $parameterInfo.Attributes.Where({$_.ParameterSetName -eq 'ByConfiguration'}).Mandatory | Should -BeTrue + } + + It 'Should accept Configuration parameter from pipeline' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSLogPath').Parameters['Configuration'] + + $parameterInfo.Attributes.ValueFromPipeline | Should -Contain $true + } + } + + Context 'When the Reporting Services instance is found' { + BeforeAll { + $mockLogPath = 'C:\Program Files\SSRS\SSRS\LogFiles' + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ErrorDumpDirectory = $mockLogPath + ServiceName = 'SQLServerReportingServices' + } + } + } + + It 'Should return the log file path' { + $result = Get-SqlDscRSLogPath -InstanceName 'SSRS' + + $result | Should -Be $mockLogPath + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } + + Context 'When the Reporting Services instance is not found' { + BeforeAll { + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return $null + } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSLogPath -InstanceName 'NonExistent' } | Should -Throw -ErrorId 'GSRSLP0001*' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } + + Context 'When the ErrorDumpDirectory is empty' { + BeforeAll { + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ErrorDumpDirectory = $null + ServiceName = 'SQLServerReportingServices' + } + } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSLogPath -InstanceName 'SSRS' } | Should -Throw -ErrorId 'GSRSLP0002*' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } + + Context 'When getting log path for Power BI Report Server' { + # cSpell: ignore PBIRS + BeforeAll { + $mockLogPath = 'C:\Program Files\PBIRS\PBIRS\LogFiles' + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'PBIRS' + InstallFolder = 'C:\Program Files\PBIRS' + ErrorDumpDirectory = $mockLogPath + ServiceName = 'PowerBIReportServer' + } + } + } + + It 'Should return the log file path for PBIRS' { + $result = Get-SqlDscRSLogPath -InstanceName 'PBIRS' + + $result | Should -Be $mockLogPath + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } + + Context 'When passing configuration via pipeline' { + BeforeAll { + $mockLogPath = 'C:\Program Files\SSRS\SSRS\LogFiles' + + $mockConfiguration = [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ErrorDumpDirectory = $mockLogPath + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ErrorDumpDirectory = $mockLogPath + ServiceName = 'SQLServerReportingServices' + } + } + } + + It 'Should return the log file path when piping configuration' { + $result = $mockConfiguration | Get-SqlDscRSLogPath + + $result | Should -Be $mockLogPath + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + + It 'Should work with Configuration parameter passed directly' { + $result = Get-SqlDscRSLogPath -Configuration $mockConfiguration + + $result | Should -Be $mockLogPath + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSServiceAccount.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSServiceAccount.Tests.ps1 new file mode 100644 index 000000000..4281c122a --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSServiceAccount.Tests.ps1 @@ -0,0 +1,95 @@ +[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 'Get-SqlDscRSServiceAccount' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSServiceAccount').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 getting service account successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + WindowsServiceIdentityActual = 'NT SERVICE\SQLServerReportingServices' + } + } + + It 'Should return the service account name' { + $result = $mockCimInstance | Get-SqlDscRSServiceAccount + + $result | Should -Be 'NT SERVICE\SQLServerReportingServices' + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + WindowsServiceIdentityActual = 'DOMAIN\ServiceAccount' + } + } + + It 'Should return the service account name' { + $result = Get-SqlDscRSServiceAccount -Configuration $mockCimInstance + + $result | Should -Be 'DOMAIN\ServiceAccount' + } + } +} diff --git a/tests/Unit/Public/New-SqlDscRSEncryptionKey.Tests.ps1 b/tests/Unit/Public/New-SqlDscRSEncryptionKey.Tests.ps1 new file mode 100644 index 000000000..0fdfb59a0 --- /dev/null +++ b/tests/Unit/Public/New-SqlDscRSEncryptionKey.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 'New-SqlDscRSEncryptionKey' { + 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 'New-SqlDscRSEncryptionKey').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 creating new encryption key successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should create new encryption key without errors' { + $null = $mockCimInstance | New-SqlDscRSEncryptionKey -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReencryptSecureInformation' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | New-SqlDscRSEncryptionKey -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When creating new encryption key with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | New-SqlDscRSEncryptionKey -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When creating new encryption key with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should create new encryption key without confirmation' { + $null = $mockCimInstance | New-SqlDscRSEncryptionKey -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 ReencryptSecureInformation() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | New-SqlDscRSEncryptionKey -Confirm:$false } | Should -Throw -ErrorId 'NSRSEK0001,New-SqlDscRSEncryptionKey' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | New-SqlDscRSEncryptionKey -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 create new encryption key' { + $null = New-SqlDscRSEncryptionKey -Configuration $mockCimInstance -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +} diff --git a/tests/Unit/Public/Remove-SqlDscRSEncryptedInformation.Tests.ps1 b/tests/Unit/Public/Remove-SqlDscRSEncryptedInformation.Tests.ps1 new file mode 100644 index 000000000..740f51b17 --- /dev/null +++ b/tests/Unit/Public/Remove-SqlDscRSEncryptedInformation.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-SqlDscRSEncryptedInformation' { + 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-SqlDscRSEncryptedInformation').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 encrypted information successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove encrypted information without errors' { + $null = $mockCimInstance | Remove-SqlDscRSEncryptedInformation -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'DeleteEncryptedInformation' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Remove-SqlDscRSEncryptedInformation -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When removing encrypted information with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Remove-SqlDscRSEncryptedInformation -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When removing encrypted information with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove encrypted information without confirmation' { + $null = $mockCimInstance | Remove-SqlDscRSEncryptedInformation -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 DeleteEncryptedInformation() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Remove-SqlDscRSEncryptedInformation -Confirm:$false } | Should -Throw -ErrorId 'RRSREI0001,Remove-SqlDscRSEncryptedInformation' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | Remove-SqlDscRSEncryptedInformation -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 encrypted information' { + $null = Remove-SqlDscRSEncryptedInformation -Configuration $mockCimInstance -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +} diff --git a/tests/Unit/Public/Remove-SqlDscRSEncryptionKey.Tests.ps1 b/tests/Unit/Public/Remove-SqlDscRSEncryptionKey.Tests.ps1 new file mode 100644 index 000000000..80b502e6b --- /dev/null +++ b/tests/Unit/Public/Remove-SqlDscRSEncryptionKey.Tests.ps1 @@ -0,0 +1,177 @@ +[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-SqlDscRSEncryptionKey' { + 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-SqlDscRSEncryptionKey').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 encrypted content successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstallationID = 'Test-Installation-ID' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove encrypted content without errors' { + $null = $mockCimInstance | Remove-SqlDscRSEncryptionKey -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'DeleteEncryptionKey' -and + $Arguments.InstallationID -eq 'Test-Installation-ID' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Remove-SqlDscRSEncryptionKey -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When removing encrypted content with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstallationID = 'Test-Installation-ID' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Remove-SqlDscRSEncryptionKey -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When removing encrypted content with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstallationID = 'Test-Installation-ID' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove encrypted content without confirmation' { + $null = $mockCimInstance | Remove-SqlDscRSEncryptionKey -Force + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstallationID = 'Test-Installation-ID' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method DeleteEncryptionKey() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Remove-SqlDscRSEncryptionKey -Confirm:$false } | Should -Throw -ErrorId 'RRSEK0001,Remove-SqlDscRSEncryptionKey' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstallationID = 'Test-Installation-ID' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | Remove-SqlDscRSEncryptionKey -WhatIf + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstallationID = 'Test-Installation-ID' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should remove encrypted content' { + $null = Remove-SqlDscRSEncryptionKey -Configuration $mockCimInstance -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +} diff --git a/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 b/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 index 8b4ff4c89..5828277db 100644 --- a/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 +++ b/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 @@ -174,4 +174,115 @@ Describe 'Request-SqlDscRSDatabaseRightsScript' { { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'NT SERVICE\SQLServerReportingServices' } | Should -Throw -ErrorId 'RSRDBRS0001,Request-SqlDscRSDatabaseRightsScript' } } + + Context 'When IsRemote is used with Windows authentication and invalid username format' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + # Mock is needed so that Should -Not -Invoke can verify it was not called + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should throw terminating error for UserName without backslash with IsRemote' { + $null = { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'SQLRSUser' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' + + Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } + } + + It 'Should throw terminating error for UserName with multiple backslashes with IsRemote' { + $null = { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'DOMAIN\SUB\SQLRSUser' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' + + Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } + } + + It 'Should throw terminating error for UserName with only domain with IsRemote' { + $null = { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'DOMAIN\' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' + + Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } + } + + It 'Should throw terminating error for UserName with only username with IsRemote' { + $null = { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName '\SQLRSUser' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' + + Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } + } + } + + Context 'When IsRemote is used with Windows authentication and valid username format' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'GRANT SELECT ON [ReportServer] TO [DOMAIN\SQLRSUser]' + } + } + } + + It 'Should not throw for valid \ format with IsRemote' { + $null = $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'DOMAIN\SQLRSUser' -IsRemote + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 + } + + It 'Should call Invoke-RsCimMethod with correct parameters for valid username' { + $null = $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'DOMAIN\SQLRSUser' -IsRemote + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' -and + $Arguments.DatabaseName -eq 'ReportServer' -and + $Arguments.UserName -eq 'DOMAIN\SQLRSUser' -and + $Arguments.IsRemote -eq $true -and + $Arguments.IsWindowsUser -eq $true + } -Exactly -Times 1 + } + } + + Context 'When IsRemote is used with SQL Server authentication' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'GRANT SELECT ON [ReportServer] TO [SqlUser]' + } + } + } + + It 'Should not validate username format with UseSqlAuthentication' { + $null = $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'SqlUser' -IsRemote -UseSqlAuthentication + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 + } + + It 'Should call Invoke-RsCimMethod with correct parameters for SQL authentication' { + $null = $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'SqlUser' -IsRemote -UseSqlAuthentication + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' -and + $Arguments.DatabaseName -eq 'ReportServer' -and + $Arguments.UserName -eq 'SqlUser' -and + $Arguments.IsRemote -eq $true -and + $Arguments.IsWindowsUser -eq $false + } -Exactly -Times 1 + } + } } diff --git a/tests/Unit/Public/Set-SqlDscRSServiceAccount.Tests.ps1 b/tests/Unit/Public/Set-SqlDscRSServiceAccount.Tests.ps1 new file mode 100644 index 000000000..2c08d8a78 --- /dev/null +++ b/tests/Unit/Public/Set-SqlDscRSServiceAccount.Tests.ps1 @@ -0,0 +1,298 @@ +[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-SqlDscRSServiceAccount' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] [-Credential] [-UseBuiltInAccount] [-RestartService] [-PassThru] [-Force] [-SuppressUrlReservationWarning] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Set-SqlDscRSServiceAccount').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 service account successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set service account without errors' { + $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetWindowsServiceIdentity' -and + $Arguments.UseBuiltInAccount -eq $false -and + $Arguments.Account -eq 'DOMAIN\ServiceAccount' + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When setting built-in service account' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'NT SERVICE\SQLServerReportingServices', + (ConvertTo-SecureString -String 'dummy' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set built-in service account' { + $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -UseBuiltInAccount -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.UseBuiltInAccount -eq $true + } -Exactly -Times 1 + } + } + + Context 'When setting service account with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When setting service account with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set service account without confirmation' { + $mockCimInstance | Set-SqlDscRSServiceAccount -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\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method SetWindowsServiceIdentity() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -Confirm:$false } | Should -Throw -ErrorId 'SSRSSA0001,Set-SqlDscRSServiceAccount' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | Set-SqlDscRSServiceAccount -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\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set service account' { + Set-SqlDscRSServiceAccount -Configuration $mockCimInstance -Credential $mockCredential -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When using RestartService switch' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\ServiceAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + Mock -CommandName Restart-SqlDscRSService + } + + It 'Should restart the service after setting the service account' { + $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -RestartService -Confirm:$false + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + Should -Invoke -CommandName Restart-SqlDscRSService -ParameterFilter { + $ServiceName -eq 'SQLServerReportingServices' -and + $Force -eq $true + } -Exactly -Times 1 + } + } + + Context 'When service account changes and SuppressUrlReservationWarning is not specified' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + WindowsServiceIdentityActual = 'DOMAIN\OldAccount' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\NewAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + Mock -CommandName Write-Warning + } + + It 'Should write a warning about URL reservations' { + $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -Confirm:$false + + Should -Invoke -CommandName Write-Warning -Exactly -Times 1 + } + } + + Context 'When service account changes and SuppressUrlReservationWarning is specified' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + WindowsServiceIdentityActual = 'DOMAIN\OldAccount' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\NewAccount', + (ConvertTo-SecureString -String 'P@ssw0rd' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + Mock -CommandName Write-Warning + } + + It 'Should not write a warning about URL reservations' { + $mockCimInstance | Set-SqlDscRSServiceAccount -Credential $mockCredential -SuppressUrlReservationWarning -Confirm:$false + + Should -Invoke -CommandName Write-Warning -Exactly -Times 0 + } + } +} diff --git a/tests/Unit/Public/Set-SqlDscRSUrlReservation.Tests.ps1 b/tests/Unit/Public/Set-SqlDscRSUrlReservation.Tests.ps1 index a0b9a54cc..48c54635e 100644 --- a/tests/Unit/Public/Set-SqlDscRSUrlReservation.Tests.ps1 +++ b/tests/Unit/Public/Set-SqlDscRSUrlReservation.Tests.ps1 @@ -49,17 +49,34 @@ Describe 'Set-SqlDscRSUrlReservation' -Tag 'Public' { $command = Get-Command -Name 'Set-SqlDscRSUrlReservation' } - It 'Should have the correct parameters in parameter set __AllParameterSets' { - $ExpectedParameters = '[-Configuration] [-Application] [-UrlString] [[-Lcid] ] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + It 'Should have the correct parameters in parameter set Set' { + $expectedParameters = '-Configuration -Application -UrlString [-Lcid ] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' $result = $command.ParameterSets | - Where-Object -FilterScript { $_.Name -eq '__AllParameterSets' } | + Where-Object -FilterScript { $_.Name -eq 'Set' } | Select-Object -Property @{ Name = 'ParameterListAsString' Expression = { $_.ToString() } } - $result.ParameterListAsString | Should -Be $ExpectedParameters + $result.ParameterListAsString | Should -Be $expectedParameters + } + + It 'Should have the correct parameters in parameter set Recreate' { + $expectedParameters = '-Configuration -RecreateExisting [-Lcid ] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + + $result = $command.ParameterSets | + Where-Object -FilterScript { $_.Name -eq 'Recreate' } | + Select-Object -Property @{ + Name = 'ParameterListAsString' + Expression = { $_.ToString() } + } + + $result.ParameterListAsString | Should -Be $expectedParameters + } + + It 'Should have Set as the default parameter set' { + $command.DefaultParameterSet | Should -Be 'Set' } } @@ -328,4 +345,203 @@ Describe 'Set-SqlDscRSUrlReservation' -Tag 'Public' { Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -Exactly -Times 0 } } + + Context 'When using RecreateExisting with multiple applications' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Get-SqlDscRSUrlReservation -MockWith { + return [PSCustomObject] @{ + HRESULT = 0 + Application = @('ReportServerWebService', 'ReportServerWebService', 'ReportServerWebApp', 'ReportServerWebApp') + UrlString = @('http://+:80', 'https://+:443', 'http://+:80', 'https://+:443') + Account = @('NT SERVICE\SSRS', 'NT SERVICE\SSRS', 'NT SERVICE\SSRS', 'NT SERVICE\SSRS') + } + } + + Mock -CommandName Add-SqlDscRSUrlReservation + Mock -CommandName Remove-SqlDscRSUrlReservation + } + + It 'Should remove and re-add all existing URL reservations' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Force + + Should -Invoke -CommandName Get-SqlDscRSUrlReservation -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -Exactly -Times 4 -Scope It + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -Exactly -Times 4 -Scope It + } + + It 'Should remove each URL reservation before re-adding' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Force + + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebService' -and $UrlString -eq 'http://+:80' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebService' -and $UrlString -eq 'https://+:443' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebApp' -and $UrlString -eq 'http://+:80' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebApp' -and $UrlString -eq 'https://+:443' + } -Exactly -Times 1 -Scope It + } + + It 'Should re-add each URL reservation' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Force + + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebService' -and $UrlString -eq 'http://+:80' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebService' -and $UrlString -eq 'https://+:443' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebApp' -and $UrlString -eq 'http://+:80' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -ParameterFilter { + $Application -eq 'ReportServerWebApp' -and $UrlString -eq 'https://+:443' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When using RecreateExisting with custom Lcid' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Get-SqlDscRSUrlReservation -MockWith { + return [PSCustomObject] @{ + HRESULT = 0 + Application = @('ReportServerWebService') + UrlString = @('http://+:80') + Account = @('NT SERVICE\SSRS') + } + } + + Mock -CommandName Add-SqlDscRSUrlReservation + Mock -CommandName Remove-SqlDscRSUrlReservation + } + + It 'Should pass Lcid to Add-SqlDscRSUrlReservation' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Lcid 1031 -Force + + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -ParameterFilter { + $Lcid -eq 1031 + } -Exactly -Times 1 + } + } + + Context 'When using RecreateExisting with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Get-SqlDscRSUrlReservation -MockWith { + return [PSCustomObject] @{ + HRESULT = 0 + Application = @('ReportServerWebService') + UrlString = @('http://+:80') + Account = @('NT SERVICE\SSRS') + } + } + + Mock -CommandName Add-SqlDscRSUrlReservation + Mock -CommandName Remove-SqlDscRSUrlReservation + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Force -PassThru + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When using RecreateExisting with no existing reservations' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Get-SqlDscRSUrlReservation -MockWith { + return [PSCustomObject] @{ + HRESULT = 0 + Application = @() + UrlString = @() + Account = @() + } + } + + Mock -CommandName Add-SqlDscRSUrlReservation + Mock -CommandName Remove-SqlDscRSUrlReservation + } + + It 'Should not call any modification commands' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Force + + Should -Invoke -CommandName Get-SqlDscRSUrlReservation -Exactly -Times 1 + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -Exactly -Times 0 + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -Exactly -Times 0 + } + } + + Context 'When using RecreateExisting with WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Get-SqlDscRSUrlReservation + Mock -CommandName Add-SqlDscRSUrlReservation + Mock -CommandName Remove-SqlDscRSUrlReservation + } + + It 'Should not call any modification commands' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -WhatIf + + Should -Invoke -CommandName Get-SqlDscRSUrlReservation -Exactly -Times 0 + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -Exactly -Times 0 + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -Exactly -Times 0 + } + } + + Context 'When using RecreateExisting with null reservations' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Get-SqlDscRSUrlReservation -MockWith { + return [PSCustomObject] @{ + HRESULT = 0 + Application = $null + UrlString = $null + Account = $null + } + } + + Mock -CommandName Add-SqlDscRSUrlReservation + Mock -CommandName Remove-SqlDscRSUrlReservation + } + + It 'Should not call any modification commands' { + $mockCimInstance | Set-SqlDscRSUrlReservation -RecreateExisting -Force + + Should -Invoke -CommandName Get-SqlDscRSUrlReservation -Exactly -Times 1 + Should -Invoke -CommandName Add-SqlDscRSUrlReservation -Exactly -Times 0 + Should -Invoke -CommandName Remove-SqlDscRSUrlReservation -Exactly -Times 0 + } + } }