From 2e2473ac751a2288bbd83eae48122aae0e9dce7a Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 10:05:12 +0100 Subject: [PATCH 01/11] Improve integration tests for RS Service Account --- CHANGELOG.md | 10 ++++++++++ azure-pipelines.yml | 11 +++++++++++ source/en-US/SqlServerDsc.strings.psd1 | 4 ++-- ...SqlDscRSEncryptedInformation.Integration.Tests.ps1 | 6 +++--- .../Request-SqlDscRSDatabaseRightsScript.Tests.ps1 | 8 ++++---- 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e716f2d5..e825fff05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -194,6 +194,16 @@ 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 command `New-SqlDscRSEncryptionKey` to delete and regenerate the + Reporting Services encryption key. Wraps the `DeleteEncryptionKey` CIM method. + Warning: This operation cannot be undone and renders all encrypted content + unreadable. +- Added public command `Remove-SqlDscRSEncryptionKey` to remove all encrypted + content from the report server database. Wraps the `DeleteEncryptionKey` CIM + method with `DeleteEncryptedContent` mode. +- Added public command `Remove-SqlDscRSEncryptedInformation` to remove all encrypted + information from the report server database, including stored credentials and + connection strings. Wraps the `DeleteEncryptedInformation` CIM method. - 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` diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 942443f5c..4784a6328 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -143,10 +143,20 @@ stages: $pesterConfig.Run.Path = '.\tests\QA' $pesterConfig.Run.Throw = $true $pesterConfig.Output.Verbosity = 'Detailed' + $pesterConfig.TestResult.Enabled = $true + $pesterConfig.TestResult.OutputFormat = 'NUnitXml' + $pesterConfig.TestResult.OutputPath = ".\$(buildFolderName)\$(testResultFolderName)\NUnit_QA.xml" Invoke-Pester -Configuration $pesterConfig name: qualityTest displayName: 'Run SqlServerDsc QA Test' + - task: PublishTestResults@2 + displayName: 'Publish Test Results' + condition: succeededOrFailed() + inputs: + testResultsFormat: 'NUnit' + testResultsFiles: '$(buildFolderName)/$(testResultFolderName)/NUnit_QA.xml' + testRunTitle: 'QA' - job: Test_HQRM displayName: 'HQRM Test' @@ -684,6 +694,7 @@ stages: # Group 8 'tests/Integration/Commands/Repair-SqlDscPowerBIReportServer.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-SqlDscPowerBIReportServer.Integration.Tests.ps1' ) diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 4a6910c91..a2fae92fd 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -827,7 +827,7 @@ ConvertFrom-StringData @' 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_SqlDscRSEncryptionKey_FailedToRemove = Failed to remove encryption key from Reporting Services instance '{0}'. (RSREK0001) ## Remove-SqlDscRSEncryptedInformation Remove_SqlDscRSEncryptedInformation_Removing = Removing encrypted information from Reporting Services instance '{0}'. @@ -835,7 +835,7 @@ ConvertFrom-StringData @' 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) + Remove_SqlDscRSEncryptedInformation_FailedToRemove = Failed to remove encrypted information from Reporting Services instance '{0}'. (RSRSEI0001) ## Get-SqlDscRSServiceAccount Get_SqlDscRSServiceAccount_Getting = Getting service account for Reporting Services instance '{0}'. diff --git a/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 b/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 index 7593c2710..db42f8e80 100644 --- a/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Remove-SqlDscRSEncryptedInformation.Integration.Tests.ps1 @@ -39,7 +39,7 @@ BeforeAll { #> Describe 'Remove-SqlDscRSEncryptedInformation' { - Context 'When removing encrypted information for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') -Skip:$true { + Context 'When removing encrypted information for SQL Server 2017 Reporting Services' -Tag @('Integration_SQL2017_RS') -Skip:$true { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } @@ -49,7 +49,7 @@ Describe 'Remove-SqlDscRSEncryptedInformation' { } } - Context 'When removing encrypted information for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + Context 'When removing encrypted information for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } @@ -59,7 +59,7 @@ Describe 'Remove-SqlDscRSEncryptedInformation' { } } - Context 'When removing encrypted information for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + Context 'When removing encrypted information for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { BeforeAll { $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' } diff --git a/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 b/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 index 5828277db..99c9d898d 100644 --- a/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 +++ b/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 @@ -186,7 +186,7 @@ Describe 'Request-SqlDscRSDatabaseRightsScript' { } 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' + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'SQLRSUser' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'GenerateDatabaseRightsScript' @@ -194,7 +194,7 @@ Describe 'Request-SqlDscRSDatabaseRightsScript' { } 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' + { $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' @@ -202,7 +202,7 @@ Describe 'Request-SqlDscRSDatabaseRightsScript' { } 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' + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'DOMAIN\' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'GenerateDatabaseRightsScript' @@ -210,7 +210,7 @@ Describe 'Request-SqlDscRSDatabaseRightsScript' { } 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' + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName '\SQLRSUser' -IsRemote } | Should -Throw -ErrorId 'UserName,New-ArgumentException' Should -Not -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'GenerateDatabaseRightsScript' From a1b92c1ee88c2a0eba364d3ede6975b927f14003 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 10:15:26 +0100 Subject: [PATCH 02/11] Add integration test for database connection in Reporting Services --- azure-pipelines.yml | 1 + ...atabaseConnection.RS.Integration.Tests.ps1 | 58 +++++++++++++++++++ ...Post.Reinitialize.RS.Integration.Tests.ps1 | 2 +- 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 4784a6328..1d1bbd509 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -589,6 +589,7 @@ stages: '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/Post.DatabaseConnection.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' diff --git a/tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 new file mode 100644 index 000000000..89fa6a339 --- /dev/null +++ b/tests/Integration/Commands/Post.DatabaseConnection.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 have issues after removing encrypted information. + + This test runs Set-SqlDscRSDatabaseConnection to re-establish the database + connection after Post.EncryptedInformation.RS.Integration.Tests.ps1 has + removed encrypted information. + + This test runs after Post.EncryptedInformation.RS.Integration.Tests.ps1 and + before Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1. +#> +Describe 'Post.DatabaseConnection.RS' { + Context 'When re-establishing database connection for SQL Server Reporting Services on SQL Server 2017' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + $script:computerName = Get-ComputerName + } + + It 'Should set the database connection' { + $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName $script:computerName -InstanceName 'RSDB' -DatabaseName 'ReportServer' -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 index 1758a4def..882ff0811 100644 --- a/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 @@ -63,7 +63,7 @@ BeforeAll { Re-add tag 'Integration_SQL2017_RS' when fixed. #> -Describe 'Post.Reinitialize.RS' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { +Describe 'Post.Reinitialize.RS' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { BeforeAll { if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') { From 59b13a73e360f57ae751e439c2b1f361dc9056f3 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 10:21:34 +0100 Subject: [PATCH 03/11] Add note for comprehensive Reporting Services log files information --- source/WikiSource/Troubleshooting-Report-Server.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/source/WikiSource/Troubleshooting-Report-Server.md b/source/WikiSource/Troubleshooting-Report-Server.md index 932d5b107..f60a2b3c5 100644 --- a/source/WikiSource/Troubleshooting-Report-Server.md +++ b/source/WikiSource/Troubleshooting-Report-Server.md @@ -21,6 +21,11 @@ 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. +> [!NOTE] +> For comprehensive information about all available Reporting Services log +> files and sources (including execution logs, trace logs, HTTP logs, and +> performance logs), see [Reporting Services log files and sources](https://learn.microsoft.com/en-us/sql/reporting-services/report-server/reporting-services-log-files-and-sources). + ### Getting the Log Path Use the `Get-SqlDscRSLogPath` command to retrieve the log folder path: From 11d12ef224d3f702a8488db34ada1b8a4c23f421 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 10:42:19 +0100 Subject: [PATCH 04/11] Add SQL Server 2017 tag to Post.ServiceAccountChange.RS integration tests --- .../Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 index 942e6246f..429abbfba 100644 --- a/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 @@ -63,7 +63,7 @@ BeforeAll { Re-add tag 'Integration_SQL2017_RS' when fixed. #> -Describe 'Post.ServiceAccountChange.RS' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { +Describe 'Post.ServiceAccountChange.RS' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { BeforeAll { if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') { From 5594da078095aa169272408bfea71707c6b14c9e Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 12:09:18 +0100 Subject: [PATCH 05/11] Add integration and unit tests for SQL Server Reporting Services DSC module - Introduced integration tests for Get-SqlDscRSUrl to validate Report Server URLs for SQL Server Reporting Services and Power BI Report Server. - Added unit tests for Get-SqlDscRSConfigFile to ensure correct XML configuration file retrieval and validation. - Updated Post.DatabaseConnection and Post.ServiceAccountChange tests to remove unnecessary comments and clarify execution order. - Refactored Get-SqlDscRSWebPortalApplicationName tests to use SetupConfiguration parameter instead of Configuration. - Removed TODO comments regarding SQL Server 2017 encryption key validation failures in Post.Reinitialize and Post.ServiceAccountChange tests. --- CHANGELOG.md | 21 + azure-pipelines.yml | 4 + source/Classes/002.ReportServerUri.ps1 | 50 +++ source/Public/Get-SqlDscRSConfigFile.ps1 | 209 +++++++++ source/Public/Get-SqlDscRSUrl.ps1 | 166 +++++++ .../Get-SqlDscRSWebPortalApplicationName.ps1 | 6 +- .../Change-Report-Server-Service-Account.md | 325 +++++++++++--- source/en-US/SqlServerDsc.strings.psd1 | 19 +- ...t-SqlDscRSConfigFile.Integration.Tests.ps1 | 131 ++++++ .../Get-SqlDscRSUrl.Integration.Tests.ps1 | 158 +++++++ ...atabaseConnection.RS.Integration.Tests.ps1 | 3 +- ...Post.Reinitialize.RS.Integration.Tests.ps1 | 19 - ...viceAccountChange.RS.Integration.Tests.ps1 | 19 - .../Public/Get-SqlDscRSConfigFile.Tests.ps1 | 419 ++++++++++++++++++ tests/Unit/Public/Get-SqlDscRSUrl.Tests.ps1 | 391 ++++++++++++++++ ...SqlDscRSWebPortalApplicationName.Tests.ps1 | 36 +- 16 files changed, 1853 insertions(+), 123 deletions(-) create mode 100644 source/Classes/002.ReportServerUri.ps1 create mode 100644 source/Public/Get-SqlDscRSConfigFile.ps1 create mode 100644 source/Public/Get-SqlDscRSUrl.ps1 create mode 100644 tests/Integration/Commands/Get-SqlDscRSConfigFile.Integration.Tests.ps1 create mode 100644 tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 create mode 100644 tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 create mode 100644 tests/Unit/Public/Get-SqlDscRSUrl.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index e825fff05..274e348c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- SqlServerDsc + - Added class `ReportServerUri` to represent URLs returned by the + `GetReportServerUrls` CIM method on `MSReportServer_Instance`. +- Added public command `Get-SqlDscRSUrl` to get the Report Server URLs for + SQL Server Reporting Services or Power BI Report Server. This command + invokes the `GetReportServerUrls` CIM method on `MSReportServer_Instance` + and returns an array of `ReportServerUri` objects containing the instance + name, application name, and URL for each configured URL. +- Added public command `Get-SqlDscRSConfigFile` to get the RsReportServer.config + configuration file for SQL Server Reporting Services (SSRS) or Power BI + Report Server (PBIRS) as an XML document object. Supports three parameter + sets: `ByInstanceName` (looks up path via registry), `ByConfiguration` + (accepts pipeline input from `Get-SqlDscRSSetupConfiguration`), and `ByPath` + (reads from a direct file path). The returned XML object supports standard + XML navigation and XPath queries for accessing configuration settings. - 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 @@ -223,6 +238,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. +- Updated wiki article `Change-Report-Server-Service-Account` with SQL Server + 2017 specific workflow. SQL Server 2017 RS requires using + `Remove-SqlDscRSEncryptedInformation` and `Set-SqlDscRSDatabaseConnection` + instead of `Remove-SqlDscRSEncryptionKey` and `New-SqlDscRSEncryptionKey` + which fail with "rsCannotValidateEncryptedData" and "Keyset does not exist" + errors on SQL Server 2017. ### Changed diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1d1bbd509..7f0452780 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -580,10 +580,12 @@ stages: 'tests/Integration/Commands/Restart-SqlDscRSService.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInitialized.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSConfigFile.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' + 'tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1' # Group 6 - Service account change 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' @@ -679,10 +681,12 @@ stages: 'tests/Integration/Commands/Restart-SqlDscRSService.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInitialized.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSLogPath.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSConfigFile.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' + 'tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1' # Group 6 - Service account change 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' diff --git a/source/Classes/002.ReportServerUri.ps1 b/source/Classes/002.ReportServerUri.ps1 new file mode 100644 index 000000000..6686657b9 --- /dev/null +++ b/source/Classes/002.ReportServerUri.ps1 @@ -0,0 +1,50 @@ +<# + .SYNOPSIS + Represents a Reporting Services URL returned by the GetReportServerUrls + CIM method. + + .DESCRIPTION + This class represents a URL for a Reporting Services application, including + the instance name, application name (such as ReportServerWebService or + ReportServerWebApp), and the URL itself. + + .PARAMETER InstanceName + The name of the Reporting Services instance. + + .PARAMETER ApplicationName + The name of the Reporting Services application. Common values include: + - ReportServerWebService + - ReportServerWebApp + - ReportManager (for older versions) + + .PARAMETER Uri + The URL for accessing the Reporting Services application. + + .EXAMPLE + [ReportServerUri]::new() + + Creates a new empty ReportServerUri instance. + + .EXAMPLE + $uri = [ReportServerUri]::new() + $uri.InstanceName = 'SSRS' + $uri.ApplicationName = 'ReportServerWebService' + $uri.Uri = 'http://localhost:80/ReportServer' + + Creates a new ReportServerUri instance with property values. +#> +class ReportServerUri +{ + [System.String] + $InstanceName + + [System.String] + $ApplicationName + + [System.String] + $Uri + + ReportServerUri() + { + } +} diff --git a/source/Public/Get-SqlDscRSConfigFile.ps1 b/source/Public/Get-SqlDscRSConfigFile.ps1 new file mode 100644 index 000000000..7165ad1f9 --- /dev/null +++ b/source/Public/Get-SqlDscRSConfigFile.ps1 @@ -0,0 +1,209 @@ +<# + .SYNOPSIS + Gets the RsReportServer.config configuration file as an XML object. + + .DESCRIPTION + Gets the RsReportServer.config configuration file for SQL Server + Reporting Services (SSRS) or Power BI Report Server (PBIRS) as an + XML document object. This allows programmatic access to configuration + settings using standard XML navigation or XPath queries. + + The configuration file path is automatically determined from the + instance's setup configuration in the registry when using the + `InstanceName` or `Configuration` parameters. Alternatively, a direct + file path can be specified using the `Path` parameter. + + .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 + or a direct path. + + .PARAMETER SetupConfiguration + Specifies the configuration object from `Get-SqlDscRSSetupConfiguration`. + This can be piped from `Get-SqlDscRSSetupConfiguration`. The object must + have a `ConfigFilePath` property containing the path to the configuration + file. This parameter accepts pipeline input. + + .PARAMETER Path + Specifies the direct path to the RsReportServer.config file. Use this + parameter to read configuration files from non-standard locations or + backup copies. + + .EXAMPLE + Get-SqlDscRSConfigFile -InstanceName 'SSRS' + + Returns the rsreportserver.config file content as an XML object for + the SSRS instance. + + .EXAMPLE + Get-SqlDscRSConfigFile -InstanceName 'PBIRS' + + Returns the rsreportserver.config file content as an XML object for + the Power BI Report Server instance. + + .EXAMPLE + Get-SqlDscRSSetupConfiguration -InstanceName 'SSRS' | Get-SqlDscRSConfigFile + + Gets the setup configuration for SSRS and pipes it to Get-SqlDscRSConfigFile + to retrieve the configuration file as XML. + + .EXAMPLE + Get-SqlDscRSConfigFile -Path 'C:\Backup\rsreportserver.config' + + Reads the configuration file from the specified path. + + .EXAMPLE + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + $config.Configuration.Service.IsSchedulingService + + Gets the config file and accesses the scheduling service setting directly + using dot notation. + + .EXAMPLE + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + $config.SelectSingleNode('//Authentication/AuthenticationTypes') + + Uses XPath to query the authentication types configuration section. + + .EXAMPLE + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + $config.SelectNodes('//Extension[@Name]') | ForEach-Object { $_.Name } + + Uses XPath to list all extension names defined in the configuration. + + .EXAMPLE + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + $config.Configuration.URLReservations.Application | + Where-Object { $_.Name -eq 'ReportServerWebService' } | + Select-Object -ExpandProperty URLs + + Gets the URL reservations for the Report Server Web Service application. + + .EXAMPLE + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + $smtpServer = $config.SelectSingleNode('//RSEmailDPConfiguration/SMTPServer') + if ($smtpServer) { $smtpServer.InnerText } + + Uses XPath to retrieve the SMTP server configuration for email delivery. + + .INPUTS + `System.Object` + + Accepts the setup configuration object from `Get-SqlDscRSSetupConfiguration` via + pipeline. + + .OUTPUTS + `System.Xml.XmlDocument` + + Returns the configuration file content as an XML document object. + + .NOTES + For more information about the RsReportServer.config configuration file, + see the Microsoft documentation: + https://learn.microsoft.com/en-us/sql/reporting-services/report-server/rsreportserver-config-configuration-file +#> +function Get-SqlDscRSConfigFile +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input and XPath the rule cannot validate.')] + [CmdletBinding(DefaultParameterSetName = 'ByInstanceName')] + [OutputType([System.Xml.XmlDocument])] + param + ( + [Parameter(Mandatory = $true, ParameterSetName = 'ByInstanceName')] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ByConfiguration')] + [System.Object] + $SetupConfiguration, + + [Parameter(Mandatory = $true, ParameterSetName = 'ByPath')] + [System.String] + $Path + ) + + process + { + $configFilePath = $null + + switch ($PSCmdlet.ParameterSetName) + { + 'ByInstanceName' + { + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSConfigFile_GettingConfigFile -f $InstanceName) + + $setupConfiguration = Get-SqlDscRSSetupConfiguration -InstanceName $InstanceName + + if (-not $setupConfiguration) + { + $errorMessage = $script:localizedData.Get_SqlDscRSConfigFile_InstanceNotFound -f $InstanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSCF0001' -ErrorCategory 'ObjectNotFound' -TargetObject $InstanceName + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + if ([System.String]::IsNullOrEmpty($setupConfiguration.ConfigFilePath)) + { + $errorMessage = $script:localizedData.Get_SqlDscRSConfigFile_ConfigFilePathNotFound -f $InstanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSCF0002' -ErrorCategory 'ObjectNotFound' -TargetObject $InstanceName + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + $configFilePath = $setupConfiguration.ConfigFilePath + } + + 'ByConfiguration' + { + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSConfigFile_GettingConfigFile -f $SetupConfiguration.InstanceName) + + if ([System.String]::IsNullOrEmpty($SetupConfiguration.ConfigFilePath)) + { + $errorMessage = $script:localizedData.Get_SqlDscRSConfigFile_ConfigFilePathNotFound -f $SetupConfiguration.InstanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSCF0002' -ErrorCategory 'ObjectNotFound' -TargetObject $SetupConfiguration.InstanceName + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + $configFilePath = $SetupConfiguration.ConfigFilePath + } + + 'ByPath' + { + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSConfigFile_ReadingFromPath -f $Path) + + if (-not (Test-Path -Path $Path -PathType 'Leaf')) + { + $errorMessage = $script:localizedData.Get_SqlDscRSConfigFile_FileNotFound -f $Path + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSCF0004' -ErrorCategory 'ObjectNotFound' -TargetObject $Path + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + $configFilePath = $Path + } + } + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSConfigFile_FoundConfigFile -f $configFilePath) + + try + { + [xml] $configXml = Get-Content -Path $configFilePath -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Get_SqlDscRSConfigFile_FailedToReadConfigFile -f $configFilePath, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ -PassThru) -ErrorId 'GSRSCF0003' -ErrorCategory 'ReadError' -TargetObject $configFilePath + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + return $configXml + } +} diff --git a/source/Public/Get-SqlDscRSUrl.ps1 b/source/Public/Get-SqlDscRSUrl.ps1 new file mode 100644 index 000000000..2757120be --- /dev/null +++ b/source/Public/Get-SqlDscRSUrl.ps1 @@ -0,0 +1,166 @@ +<# + .SYNOPSIS + Gets the Report Server URLs for a SQL Server Reporting Services instance. + + .DESCRIPTION + Gets the Report Server URLs for SQL Server Reporting Services or Power BI + Report Server by invoking the `GetReportServerUrls` CIM method on the + `MSReportServer_Instance` CIM class. This returns the URLs for all + configured Reporting Services applications (such as ReportServerWebService + and ReportServerWebApp). + + The setup configuration can be obtained using `Get-SqlDscRSSetupConfiguration` + and passed via the pipeline. + + .PARAMETER SetupConfiguration + Specifies the setup configuration object for the Reporting Services + instance. This can be obtained using `Get-SqlDscRSSetupConfiguration`. + This parameter accepts pipeline input. + + .EXAMPLE + Get-SqlDscRSSetupConfiguration -InstanceName 'SSRS' | Get-SqlDscRSUrl + + Gets the Report Server URLs for the Reporting Services instance 'SSRS'. + + .EXAMPLE + Get-SqlDscRSSetupConfiguration | Get-SqlDscRSUrl + + Gets the Report Server URLs for all Reporting Services instances. + + .EXAMPLE + $urls = Get-SqlDscRSSetupConfiguration -InstanceName 'SSRS' | Get-SqlDscRSUrl + $urls | Where-Object -FilterScript { $_.ApplicationName -eq 'ReportServerWebService' } + + Gets only the ReportServerWebService URLs for the instance 'SSRS'. + + .INPUTS + `System.Object` + + Accepts setup configuration objects from `Get-SqlDscRSSetupConfiguration` + via pipeline. + + .OUTPUTS + `[ReportServerUri[]]` + + Returns an array of ReportServerUri objects containing the instance name, + application name, and URL. Returns `$null` if no URLs are configured. + + .NOTES + The Reporting Services instance must have URLs configured via + `Set-SqlDscRSUrlReservation` before URLs will be returned by this command. + + .LINK + https://learn.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/msreportserver-instance-methods-getreportserverurls +#> +function Get-SqlDscRSUrl +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding()] + [OutputType([ReportServerUri[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $SetupConfiguration + ) + + process + { + $instanceName = $SetupConfiguration.InstanceName + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSUrl_GettingUrls -f $instanceName) + + # Validate that CurrentVersion is available + if ([System.String]::IsNullOrEmpty($SetupConfiguration.CurrentVersion)) + { + $errorMessage = $script:localizedData.Get_SqlDscRSUrl_VersionNotFound -f $instanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSU0001' -ErrorCategory 'InvalidOperation' -TargetObject $SetupConfiguration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + # Construct the CIM namespace for MSReportServer_Instance + $version = ([System.Version] $SetupConfiguration.CurrentVersion).Major + $namespace = 'root\Microsoft\SqlServer\ReportServer\RS_{0}\v{1}' -f $instanceName, $version + + try + { + # Get the MSReportServer_Instance CIM instance + $getCimInstanceParameters = @{ + Namespace = $namespace + ClassName = 'MSReportServer_Instance' + Filter = "InstanceId='{0}'" -f $SetupConfiguration.InstanceId + ErrorAction = 'Stop' + } + + $msReportServerInstance = Get-CimInstance @getCimInstanceParameters + } + catch + { + $errorMessage = $script:localizedData.Get_SqlDscRSUrl_FailedToGetInstance -f $instanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSU0002' -ErrorCategory 'InvalidOperation' -TargetObject $SetupConfiguration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + if (-not $msReportServerInstance) + { + $errorMessage = $script:localizedData.Get_SqlDscRSUrl_InstanceNotFound -f $instanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSU0003' -ErrorCategory 'InvalidOperation' -TargetObject $SetupConfiguration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + try + { + # Invoke the GetReportServerUrls method + $invokeRsCimMethodParameters = @{ + CimInstance = $msReportServerInstance + MethodName = 'GetReportServerUrls' + } + + $result = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Get_SqlDscRSUrl_FailedToGetUrls -f $instanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSU0004' -ErrorCategory 'InvalidOperation' -TargetObject $SetupConfiguration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + # Check if any URLs were returned + if (-not $result.ApplicationName -or $result.ApplicationName.Count -eq 0) + { + return $null + } + + # Build the result array + $reportServerUrls = @() + + for ($i = 0; $i -lt $result.ApplicationName.Count; $i++) + { + $applicationName = $result.ApplicationName[$i] + $urls = $result.URLs[$i] + + # Each application can have multiple URLs + foreach ($url in $urls) + { + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSUrl_FoundUrl -f $applicationName, $url) + + $reportServerUrl = [ReportServerUri]::new() + $reportServerUrl.InstanceName = $instanceName + $reportServerUrl.ApplicationName = $applicationName + $reportServerUrl.Uri = $url + + $reportServerUrls += $reportServerUrl + } + } + + return $reportServerUrls + } +} diff --git a/source/Public/Get-SqlDscRSWebPortalApplicationName.ps1 b/source/Public/Get-SqlDscRSWebPortalApplicationName.ps1 index 15b542f53..a018a2454 100644 --- a/source/Public/Get-SqlDscRSWebPortalApplicationName.ps1 +++ b/source/Public/Get-SqlDscRSWebPortalApplicationName.ps1 @@ -13,7 +13,7 @@ See more information at: https://docs.microsoft.com/en-us/sql/reporting-services/breaking-changes-in-sql-server-reporting-services-in-sql-server-2016 - .PARAMETER Configuration + .PARAMETER SetupConfiguration Specifies the setup configuration object for the Reporting Services instance. This can be obtained using the `Get-SqlDscRSSetupConfiguration` command. This parameter accepts pipeline input. @@ -44,14 +44,14 @@ function Get-SqlDscRSWebPortalApplicationName ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [System.Object] - $Configuration + $SetupConfiguration ) process { Write-Debug -Message $script:localizedData.Get_SqlDscRSWebPortalApplicationName_GettingApplicationName - $sqlVersion = $Configuration | Get-SqlDscRSVersion + $sqlVersion = $SetupConfiguration | Get-SqlDscRSVersion if (-not $sqlVersion) { diff --git a/source/WikiSource/Change-Report-Server-Service-Account.md b/source/WikiSource/Change-Report-Server-Service-Account.md index 6a4e7af9a..abcfd2c80 100644 --- a/source/WikiSource/Change-Report-Server-Service-Account.md +++ b/source/WikiSource/Change-Report-Server-Service-Account.md @@ -64,18 +64,21 @@ Before starting, ensure you have: ## 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` | +in order. **Steps 4 and 5 differ for SQL Server 2017 RS** — see the notes in +those steps for version-specific commands. + + +| Step | Action | SQL Server 2019/2022 RS & Power BI | SQL Server 2017 RS | +| --- | --- | --- | --- | +| 1 | Change the service account | `Set-SqlDscRSServiceAccount` | Same | +| 2 | Verify the change | `Get-SqlDscRSServiceAccount` | Same | +| 3 | Grant database permissions | `New-SqlDscLogin`, `Request-SqlDscRSDatabaseRightsScript`, `Invoke-SqlDscQuery` | Same | +| 4 | Handle encryption | `Remove-SqlDscRSEncryptionKey` | `Remove-SqlDscRSEncryptedInformation` | +| 5 | Restore encryption capability | `New-SqlDscRSEncryptionKey` | `Set-SqlDscRSDatabaseConnection` | +| 6 | Recreate URL reservations | `Set-SqlDscRSUrlReservation -RecreateExisting` | Same | +| 7 | Re-initialize the Report Server | `Initialize-SqlDscRS` | Same | +| 8 | Validate accessibility | `Test-SqlDscRSAccessible` | Same | + ## Step 1: Change the Service Account @@ -180,17 +183,23 @@ Request the database rights script from Report Server and execute it: $databaseName = $configuration.DatabaseName # Generate the T-SQL script that grants required permissions -$databaseRightsScript = $configuration | Request-SqlDscRSDatabaseRightsScript ` - -DatabaseName $databaseName ` - -UserName $currentServiceAccount +$requestSqlDscRSDatabaseRightsScriptParams = @{ + DatabaseName = $databaseName + UserName = $currentServiceAccount +} + +$databaseRightsScript = $configuration | Request-SqlDscRSDatabaseRightsScript @requestSqlDscRSDatabaseRightsScriptParams # Execute the script on the SQL Server -Invoke-SqlDscQuery ` - -ServerName 'localhost' ` - -InstanceName 'RSDB' ` - -DatabaseName 'master' ` - -Query $databaseRightsScript ` - -Force +$invokeSqlDscQueryParams = @{ + ServerName = 'localhost' + InstanceName = 'RSDB' + DatabaseName = 'master' + Query = $databaseRightsScript + Force = $true +} + +Invoke-SqlDscQuery @invokeSqlDscQueryParams ``` @@ -209,12 +218,21 @@ $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 +## Step 4: Handle Encryption > [!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. +> This is a destructive operation. Once you remove the encryption key or encrypted +> information, the old data cannot be recovered. Any data encrypted with the old +> key (stored credentials, connection strings) will be inaccessible. + +**What this affects:** + +- Stored credentials for data sources +- Connection strings with embedded credentials +- Subscription delivery settings with credentials +- Unattended execution account credentials + +### For SQL Server 2019/2022 RS and Power BI Report Server Remove the existing encryption key that was tied to the old service account: @@ -225,16 +243,28 @@ $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 +### For SQL Server 2017 RS Only + +> [!CAUTION] +> **Do NOT run `Remove-SqlDscRSEncryptionKey` on SQL Server 2017 RS.** This command +> fails with "rsCannotValidateEncryptedData" and "Keyset does not exist" errors +> and will leave the Report Server in an inconsistent state. + +Instead, remove all encrypted information from the database: + +```powershell +$configuration | Remove-SqlDscRSEncryptedInformation -Force +``` + +This command removes all encrypted data from the Report Server database as a +workaround for the encryption key validation issue in SQL Server 2017. + +## Step 5: Restore Encryption Capability + +### For SQL Server 2019/2022 RS and Power BI Report Server Generate a new encryption key tied to the new service account: @@ -255,6 +285,37 @@ After this step, the Report Server can again encrypt and decrypt sensitive data. > encryption key using `Backup-SqlDscRSEncryptionKey`. This backup is critical > for disaster recovery scenarios. +### For SQL Server 2017 RS Only + +> [!CAUTION] +> **Do NOT run `New-SqlDscRSEncryptionKey` on SQL Server 2017 RS.** This command +> fails with "rsCannotValidateEncryptedData" and "Keyset does not exist" errors. + +Instead, re-establish the database connection to restore encryption capability: + +```powershell +$setSqlDscRSDatabaseConnectionParams = @{ + ServerName = 'localhost' + InstanceName = 'RSDB' + DatabaseName = 'ReportServer' + Force = $true +} + +$configuration | Set-SqlDscRSDatabaseConnection @setSqlDscRSDatabaseConnectionParams +``` + +> [!NOTE] +> Replace `'localhost'`, `'RSDB'`, and `'ReportServer'` with your actual SQL +> Server name, instance name, and Report Server database name. + +This command re-establishes the database connection after removing encrypted +information, allowing the Report Server to function with the new service account. + +> [!IMPORTANT] +> **SQL Server 2017 trade-off:** All previously stored credentials are lost. +> After completing the service account change, you must manually re-enter all +> credentials for data sources, subscriptions, and the unattended execution account. + ## Step 6: Recreate URL Reservations URL reservations in HTTP.sys are registered with the service account's Security @@ -358,11 +419,14 @@ Write-Information -MessageData "Current service account: $($configuration.Window Write-Information -MessageData "`n[Step 1] Changing service account..." -InformationAction 'Continue' -$configuration | Set-SqlDscRSServiceAccount ` - -Credential $newCredential ` - -RestartService ` - -SuppressUrlReservationWarning ` - -Force +$setSqlDscRSServiceAccountParams = @{ + Credential = $newCredential + RestartService = $true + SuppressUrlReservationWarning = $true + Force = $true +} + +$configuration | Set-SqlDscRSServiceAccount @setSqlDscRSServiceAccountParams # ============================================================================ # STEP 2: Verify the change @@ -388,16 +452,23 @@ 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 +$requestSqlDscRSDatabaseRightsScriptParams = @{ + DatabaseName = $databaseName + UserName = $currentServiceAccount +} + +$databaseRightsScript = $configuration | Request-SqlDscRSDatabaseRightsScript @requestSqlDscRSDatabaseRightsScriptParams + +$invokeSqlDscQueryParams = @{ + ServerName = $dbServerName + InstanceName = $dbInstanceName + DatabaseName = 'master' + Query = $databaseRightsScript + Force = $true +} + +Invoke-SqlDscQuery @invokeSqlDscQueryParams # Restart service to apply permissions $configuration | Restart-SqlDscRSService -Force @@ -405,7 +476,10 @@ $configuration | Restart-SqlDscRSService -Force Write-Information -MessageData "Database permissions granted" -InformationAction 'Continue' # ============================================================================ -# STEP 4: Remove old encryption key +# STEP 4: Handle encryption (version-specific) +# ============================================================================ +# NOTE: For SQL Server 2017 RS, use Remove-SqlDscRSEncryptedInformation instead. +# See the SQL Server 2017 script below for the alternative workflow. # ============================================================================ Write-Information -MessageData "`n[Step 4] Removing old encryption key..." -InformationAction 'Continue' @@ -415,7 +489,10 @@ $configuration | Remove-SqlDscRSEncryptionKey -Force Write-Information -MessageData "Old encryption key removed" -InformationAction 'Continue' # ============================================================================ -# STEP 5: Create new encryption key +# STEP 5: Restore encryption capability (version-specific) +# ============================================================================ +# NOTE: For SQL Server 2017 RS, use Set-SqlDscRSDatabaseConnection instead. +# See the SQL Server 2017 script below for the alternative workflow. # ============================================================================ Write-Information -MessageData "`n[Step 5] Creating new encryption key..." -InformationAction 'Continue' @@ -477,20 +554,145 @@ Write-Information -MessageData "`n[Complete] Service account change finished suc 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 +### Complete SQL Server 2017 Script -On SQL Server 2017 Reporting Services, the encryption key operations (Steps 4-5) -may fail with the error: +The following script is specifically for SQL Server 2017 RS, which uses +`Remove-SqlDscRSEncryptedInformation` and `Set-SqlDscRSDatabaseConnection` +instead of the encryption key commands used for SQL Server 2019/2022 RS and +Power BI Report Server. -```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) -``` + +```powershell +#Requires -Modules SqlServerDsc +#Requires -RunAsAdministrator + +# ============================================================================ +# SQL SERVER 2017 REPORTING SERVICES - Service Account Change Script +# ============================================================================ +# This script is specifically for SQL Server 2017 RS which requires a +# different workflow than SQL Server 2019/2022 and Power BI Report Server. +# ============================================================================ + +# CONFIGURATION - Customize these values for your environment +$instanceName = 'SSRS' +$dbServerName = 'localhost' +$dbInstanceName = 'RSDB' +$databaseName = 'ReportServer' + +# 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' + +$setSqlDscRSServiceAccountParams = @{ + Credential = $newCredential + RestartService = $true + SuppressUrlReservationWarning = $true + Force = $true +} + +$configuration | Set-SqlDscRSServiceAccount @setSqlDscRSServiceAccountParams + +# 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' + +$serverObject = Connect-SqlDscDatabaseEngine -ServerName $dbServerName -InstanceName $dbInstanceName +New-SqlDscLogin -ServerObject $serverObject -Name $currentServiceAccount -WindowsUser -Force +Disconnect-SqlDscDatabaseEngine -ServerObject $serverObject + +$requestSqlDscRSDatabaseRightsScriptParams = @{ + DatabaseName = $databaseName + UserName = $currentServiceAccount +} + +$databaseRightsScript = $configuration | Request-SqlDscRSDatabaseRightsScript @requestSqlDscRSDatabaseRightsScriptParams + +$invokeSqlDscQueryParams = @{ + ServerName = $dbServerName + InstanceName = $dbInstanceName + DatabaseName = 'master' + Query = $databaseRightsScript + Force = $true +} + +Invoke-SqlDscQuery @invokeSqlDscQueryParams + +$configuration | Restart-SqlDscRSService -Force + +Write-Information -MessageData "Database permissions granted" -InformationAction 'Continue' + +# STEP 4 (SQL 2017): Remove encrypted information +# NOTE: Do NOT use Remove-SqlDscRSEncryptionKey on SQL Server 2017! +Write-Information -MessageData "`n[Step 4] Removing encrypted information (SQL 2017 workaround)..." -InformationAction 'Continue' + +$configuration | Remove-SqlDscRSEncryptedInformation -Force + +Write-Information -MessageData "Encrypted information removed" -InformationAction 'Continue' + +# STEP 5 (SQL 2017): Re-establish database connection +# NOTE: Do NOT use New-SqlDscRSEncryptionKey on SQL Server 2017! +Write-Information -MessageData "`n[Step 5] Re-establishing database connection (SQL 2017 workaround)..." -InformationAction 'Continue' + +$setSqlDscRSDatabaseConnectionParams = @{ + ServerName = $dbServerName + InstanceName = $dbInstanceName + DatabaseName = $databaseName + Force = $true +} + +$configuration | Set-SqlDscRSDatabaseConnection @setSqlDscRSDatabaseConnectionParams + +Write-Information -MessageData "Database connection re-established" -InformationAction 'Continue' -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. +# 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] SQL Server 2017 service account change finished!" -InformationAction 'Continue' +Write-Information -MessageData "`n[ACTION REQUIRED] Re-enter all stored credentials for data sources and subscriptions." -InformationAction 'Continue' +``` + ### Test in Non-Production First @@ -520,6 +722,9 @@ 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. **SQL Server 2017 RS requires different commands for Steps 4-5** — use + `Remove-SqlDscRSEncryptedInformation` and `Set-SqlDscRSDatabaseConnection` + instead of the encryption key commands +1. Encryption key/data 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/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index a2fae92fd..88cdfdc64 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -288,6 +288,14 @@ ConvertFrom-StringData @' Get_SqlDscRSSetupConfiguration_InstanceNotFound = Could not find a Microsoft SQL Server Reporting Services instance with the name '{0}'. Get_SqlDscRSSetupConfiguration_NoInstancesFound = No SQL Server Reporting Services instances were found. + ## Get-SqlDscRSUrl + Get_SqlDscRSUrl_GettingUrls = Getting Report Server URLs for Reporting Services instance '{0}'. + Get_SqlDscRSUrl_FoundUrl = Found URL for application '{0}': {1} + Get_SqlDscRSUrl_VersionNotFound = Could not determine the version for Reporting Services instance '{0}'. The CurrentVersion property is empty. (GSRSU0001) + Get_SqlDscRSUrl_FailedToGetInstance = Failed to get the MSReportServer_Instance CIM instance for Reporting Services instance '{0}': {1} (GSRSU0002) + Get_SqlDscRSUrl_InstanceNotFound = Could not find the MSReportServer_Instance CIM instance for Reporting Services instance '{0}'. (GSRSU0003) + Get_SqlDscRSUrl_FailedToGetUrls = Failed to get Report Server URLs for Reporting Services instance '{0}': {1} (GSRSU0004) + ## Get-SqlDscRSConfiguration Get_SqlDscRSConfiguration_DetectingVersion = Detecting version for Reporting Services instance '{0}'. Get_SqlDscRSConfiguration_GettingConfiguration = Getting configuration CIM instance for Reporting Services instance '{0}' version '{1}'. @@ -297,12 +305,20 @@ ConvertFrom-StringData @' 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-SqlDscRSConfigFile + Get_SqlDscRSConfigFile_GettingConfigFile = Getting configuration file for Reporting Services instance '{0}'. + Get_SqlDscRSConfigFile_ReadingFromPath = Reading configuration file from path '{0}'. + Get_SqlDscRSConfigFile_FoundConfigFile = Found configuration file at path: '{0}'. + Get_SqlDscRSConfigFile_InstanceNotFound = Could not find a Reporting Services instance with the name '{0}'. (GSRSCF0001) + Get_SqlDscRSConfigFile_ConfigFilePathNotFound = Could not determine the configuration file path for Reporting Services instance '{0}'. The ConfigFilePath registry value is empty. (GSRSCF0002) + Get_SqlDscRSConfigFile_FailedToReadConfigFile = Failed to read the configuration file '{0}': {1} (GSRSCF0003) + Get_SqlDscRSConfigFile_FileNotFound = Could not find the configuration file at path '{0}'. (GSRSCF0004) + ## Get-SqlDscRSWebPortalApplicationName Get_SqlDscRSWebPortalApplicationName_GettingApplicationName = Getting web portal application name. @@ -332,7 +348,6 @@ ConvertFrom-StringData @' Invoke_RsCimMethod_NoErrorDetails = No error details were returned by the method. See HRESULT code for more information. (IRCM0002) ## Get-HResultMessage - # cSpell: ignore GHRM HResult_AccessDenied = Access is denied. Verify that the current user has administrator rights on the Reporting Services instance. (GHRM0001) HResult_LogonTypeNotGranted = The account has not been granted the requested logon type at this computer. Verify that the Reporting Services service is running and that the service account has the required permissions to interact with the Reporting Services WMI provider. (GHRM0002) HResult_UnspecifiedFailure = An unspecified failure occurred. (GHRM0003) diff --git a/tests/Integration/Commands/Get-SqlDscRSConfigFile.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSConfigFile.Integration.Tests.ps1 new file mode 100644 index 000000000..e5d6f2472 --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSConfigFile.Integration.Tests.ps1 @@ -0,0 +1,131 @@ +[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-SqlDscRSConfigFile' { + Context 'When getting the configuration file for SQL Server Reporting Services instance' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS') { + It 'Should return the configuration file as an XML object for SSRS instance' { + $result = Get-SqlDscRSConfigFile -InstanceName 'SSRS' -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + } + + It 'Should return a configuration file with expected structure' { + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Verify key configuration sections exist + $config.Configuration.Dsn | Should -Not -BeNullOrEmpty + $config.Configuration.InstanceId | Should -Not -BeNullOrEmpty + $config.Configuration.Service | Should -Not -BeNullOrEmpty + $config.Configuration.Authentication | Should -Not -BeNullOrEmpty + } + + It 'Should support XPath queries on the returned XML' { + $config = Get-SqlDscRSConfigFile -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Use XPath to query authentication types + $authTypes = $config.SelectSingleNode('//Authentication/AuthenticationTypes') + + $authTypes | Should -Not -BeNullOrEmpty + } + + It 'Should work with pipeline input from Get-SqlDscRSSetupConfiguration' { + $result = Get-SqlDscRSSetupConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' | Get-SqlDscRSConfigFile -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + } + + It 'Should work with Path parameter using the config file path' { + $setupConfig = Get-SqlDscRSSetupConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $configFilePath = $setupConfig.ConfigFilePath + + $result = Get-SqlDscRSConfigFile -Path $configFilePath -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + } + } + + Context 'When getting the configuration file for Power BI Report Server instance' -Tag @('Integration_PowerBI') { + # cSpell: ignore PBIRS + It 'Should return the configuration file as an XML object for PBIRS instance' { + $result = Get-SqlDscRSConfigFile -InstanceName 'PBIRS' -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + } + + It 'Should return a configuration file with expected structure' { + $config = Get-SqlDscRSConfigFile -InstanceName 'PBIRS' -ErrorAction 'Stop' + + # Verify key configuration sections exist + $config.Configuration.Dsn | Should -Not -BeNullOrEmpty + $config.Configuration.InstanceId | Should -Not -BeNullOrEmpty + $config.Configuration.Service | Should -Not -BeNullOrEmpty + } + + It 'Should work with pipeline input from Get-SqlDscRSSetupConfiguration' { + $result = Get-SqlDscRSSetupConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' | Get-SqlDscRSConfigFile -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + } + + It 'Should work with Path parameter using the config file path' { + $setupConfig = Get-SqlDscRSSetupConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + $configFilePath = $setupConfig.ConfigFilePath + + $result = Get-SqlDscRSConfigFile -Path $configFilePath -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + } + } + + Context 'When trying to get the configuration file for a non-existent instance' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -InstanceName 'NonExistentInstance' -ErrorAction 'Stop' } | Should -Throw + } + } + + Context 'When trying to read a non-existent file path' -Tag @('Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -Path 'C:\NonExistent\rsreportserver.config' -ErrorAction 'Stop' } | Should -Throw + } + } +} diff --git a/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 new file mode 100644 index 000000000..6f03301e1 --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 @@ -0,0 +1,158 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + # Do not use -Force. Doing so, or unloading the module in AfterAll, causes + # PowerShell class types to get new identities, breaking type comparisons. + Import-Module -Name $script:moduleName -ErrorAction 'Stop' +} + +Describe 'Get-SqlDscRSUrl' { + Context 'When getting Report Server URLs for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS') { + BeforeAll { + $script:setupConfiguration = Get-SqlDscRSSetupConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should return Report Server URLs using pipeline' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + } + + It 'Should return Report Server URLs using SetupConfiguration parameter' { + $result = Get-SqlDscRSUrl -SetupConfiguration $script:setupConfiguration -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + } + + It 'Should return ReportServerUri objects with expected properties' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result[0] | Should -BeOfType 'ReportServerUri' + $result[0].PSObject.Properties.Name | Should -Contain 'InstanceName' + $result[0].PSObject.Properties.Name | Should -Contain 'ApplicationName' + $result[0].PSObject.Properties.Name | Should -Contain 'Uri' + } + + It 'Should return URLs for the correct instance' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | ForEach-Object -Process { + $_.InstanceName | Should -Be 'SSRS' + } + } + + It 'Should return URLs for ReportServerWebService application' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $webServiceUrls = $result | Where-Object -FilterScript { + $_.ApplicationName -eq 'ReportServerWebService' + } + + $webServiceUrls | Should -Not -BeNullOrEmpty -Because 'ReportServerWebService should have URLs configured' + $webServiceUrls[0].Uri | Should -Match '^https?://' -Because 'URL should be a valid HTTP or HTTPS URL' + } + + It 'Should return URLs for ReportServerWebApp application' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $webAppUrls = $result | Where-Object -FilterScript { + $_.ApplicationName -eq 'ReportServerWebApp' + } + + $webAppUrls | Should -Not -BeNullOrEmpty -Because 'ReportServerWebApp should have URLs configured' + $webAppUrls[0].Uri | Should -Match '^https?://' -Because 'URL should be a valid HTTP or HTTPS URL' + } + } + + Context 'When getting Report Server URLs for Power BI Report Server' -Tag @('Integration_PBIRS') { + BeforeAll { + $script:setupConfiguration = Get-SqlDscRSSetupConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + } + + It 'Should return Report Server URLs using pipeline' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + } + + It 'Should return Report Server URLs using SetupConfiguration parameter' { + $result = Get-SqlDscRSUrl -SetupConfiguration $script:setupConfiguration -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + } + + It 'Should return ReportServerUri objects with expected properties' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result[0] | Should -BeOfType 'ReportServerUri' + $result[0].PSObject.Properties.Name | Should -Contain 'InstanceName' + $result[0].PSObject.Properties.Name | Should -Contain 'ApplicationName' + $result[0].PSObject.Properties.Name | Should -Contain 'Uri' + } + + It 'Should return URLs for the correct instance' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | ForEach-Object -Process { + $_.InstanceName | Should -Be 'PBIRS' + } + } + + It 'Should return URLs for ReportServerWebService application' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $webServiceUrls = $result | Where-Object -FilterScript { + $_.ApplicationName -eq 'ReportServerWebService' + } + + $webServiceUrls | Should -Not -BeNullOrEmpty -Because 'ReportServerWebService should have URLs configured' + $webServiceUrls[0].Uri | Should -Match '^https?://' -Because 'URL should be a valid HTTP or HTTPS URL' + } + + It 'Should return URLs for ReportServerWebApp application' { + $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $webAppUrls = $result | Where-Object -FilterScript { + $_.ApplicationName -eq 'ReportServerWebApp' + } + + $webAppUrls | Should -Not -BeNullOrEmpty -Because 'ReportServerWebApp should have URLs configured' + $webAppUrls[0].Uri | Should -Match '^https?://' -Because 'URL should be a valid HTTP or HTTPS URL' + } + } + + Context 'When getting Report Server URLs for all instances' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS') { + It 'Should return URLs for all instances via pipeline' { + $result = Get-SqlDscRSSetupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + } + } +} diff --git a/tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 index 89fa6a339..7e0f6542d 100644 --- a/tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Post.DatabaseConnection.RS.Integration.Tests.ps1 @@ -40,8 +40,7 @@ BeforeAll { connection after Post.EncryptedInformation.RS.Integration.Tests.ps1 has removed encrypted information. - This test runs after Post.EncryptedInformation.RS.Integration.Tests.ps1 and - before Remove-SqlDscRSEncryptionKey.Integration.Tests.ps1. + This test runs after Post.EncryptedInformation.RS.Integration.Tests.ps1. #> Describe 'Post.DatabaseConnection.RS' { Context 'When re-establishing database connection for SQL Server Reporting Services on SQL Server 2017' -Tag @('Integration_SQL2017_RS') { diff --git a/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 index 882ff0811..f3971e1c0 100644 --- a/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Post.Reinitialize.RS.Integration.Tests.ps1 @@ -44,25 +44,6 @@ BeforeAll { 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_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { BeforeAll { if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') diff --git a/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 b/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 index 429abbfba..74b8d847d 100644 --- a/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Post.ServiceAccountChange.RS.Integration.Tests.ps1 @@ -44,25 +44,6 @@ BeforeAll { 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_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { BeforeAll { if (Test-ContinuousIntegrationTaskCategory -Category 'Integration_PowerBI') diff --git a/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 new file mode 100644 index 000000000..d47c62ff9 --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 @@ -0,0 +1,419 @@ +[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' +} + +# cSpell:ignore MSRS NTLM +Describe 'Get-SqlDscRSConfigFile' { + Context 'When parameter validation' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = 'ByInstanceName' + ExpectedParameters = '-InstanceName []' + } + @{ + ExpectedParameterSetName = 'ByConfiguration' + ExpectedParameters = '-SetupConfiguration []' + } + @{ + ExpectedParameterSetName = 'ByPath' + ExpectedParameters = '-Path []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSConfigFile').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 ByInstanceName as the default parameter set' { + $command = Get-Command -Name 'Get-SqlDscRSConfigFile' + + $command.DefaultParameterSet | Should -Be 'ByInstanceName' + } + + It 'Should have InstanceName as a mandatory parameter in the ByInstanceName parameter set' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfigFile').Parameters['InstanceName'] + + $parameterInfo.Attributes.Where({$_.ParameterSetName -eq 'ByInstanceName'}).Mandatory | Should -BeTrue + } + + It 'Should have SetupConfiguration as a mandatory parameter in the ByConfiguration parameter set' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfigFile').Parameters['SetupConfiguration'] + + $parameterInfo.Attributes.Where({$_.ParameterSetName -eq 'ByConfiguration'}).Mandatory | Should -BeTrue + } + + It 'Should accept SetupConfiguration parameter from pipeline' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfigFile').Parameters['SetupConfiguration'] + + $parameterInfo.Attributes.ValueFromPipeline | Should -Contain $true + } + + It 'Should have Path as a mandatory parameter in the ByPath parameter set' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSConfigFile').Parameters['Path'] + + $parameterInfo.Attributes.Where({$_.ParameterSetName -eq 'ByPath'}).Mandatory | Should -BeTrue + } + } + + Context 'When using InstanceName parameter and the instance is found' { + BeforeAll { + $mockConfigFilePath = 'C:\Program Files\SSRS\SSRS\ReportServer\RSReportServer.config' + $mockConfigXmlContent = @' + + + TestDsn + Default + MSRS13.SSRS + + True + True + + + + + + + +'@ + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ConfigFilePath = $mockConfigFilePath + ServiceName = 'SQLServerReportingServices' + } + } + + Mock -CommandName Get-Content -MockWith { + return $mockConfigXmlContent + } + } + + It 'Should return the configuration file as an XML object' { + $result = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration | Should -Not -BeNullOrEmpty + $result.Configuration.Dsn | Should -Be 'TestDsn' + $result.Configuration.InstanceId | Should -Be 'MSRS13.SSRS' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Content -Exactly -Times 1 -Scope It + } + + It 'Should allow accessing nested XML elements' { + $result = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + + $result.Configuration.Service.IsSchedulingService | Should -Be 'True' + # Self-closing XML elements exist as empty string, so check with SelectSingleNode + $result.SelectSingleNode('//Authentication/AuthenticationTypes/RSWindowsNTLM') | Should -Not -BeNullOrEmpty + } + } + + Context 'When using InstanceName parameter and the instance is not found' { + BeforeAll { + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return $null + } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -InstanceName 'NonExistent' } | Should -Throw -ErrorId 'GSRSCF0001*' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } + + Context 'When using InstanceName parameter and the ConfigFilePath is empty' { + BeforeAll { + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ConfigFilePath = $null + ServiceName = 'SQLServerReportingServices' + } + } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -InstanceName 'SSRS' } | Should -Throw -ErrorId 'GSRSCF0002*' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + } + } + + Context 'When using InstanceName parameter and reading the file fails' { + BeforeAll { + $mockConfigFilePath = 'C:\Program Files\SSRS\SSRS\ReportServer\RSReportServer.config' + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ConfigFilePath = $mockConfigFilePath + ServiceName = 'SQLServerReportingServices' + } + } + + Mock -CommandName Get-Content -MockWith { + throw 'Access denied' + } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -InstanceName 'SSRS' } | Should -Throw -ErrorId 'GSRSCF0003*' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Content -Exactly -Times 1 -Scope It + } + } + + Context 'When using SetupConfiguration parameter via pipeline' { + BeforeAll { + $mockConfigFilePath = 'C:\Program Files\SSRS\SSRS\ReportServer\RSReportServer.config' + $mockConfigXmlContent = @' + + + TestDsn + MSRS13.SSRS + +'@ + + $mockSetupConfiguration = [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ConfigFilePath = $mockConfigFilePath + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Get-Content -MockWith { + return $mockConfigXmlContent + } + } + + It 'Should return the configuration file when piping SetupConfiguration' { + $result = $mockSetupConfiguration | Get-SqlDscRSConfigFile + + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration.Dsn | Should -Be 'TestDsn' + + Should -Invoke -CommandName Get-Content -Exactly -Times 1 -Scope It + } + + It 'Should work with SetupConfiguration parameter passed directly' { + $result = Get-SqlDscRSConfigFile -SetupConfiguration $mockSetupConfiguration + + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration.InstanceId | Should -Be 'MSRS13.SSRS' + + Should -Invoke -CommandName Get-Content -Exactly -Times 1 -Scope It + } + } + + Context 'When using SetupConfiguration parameter and ConfigFilePath is empty' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ConfigFilePath = $null + ServiceName = 'SQLServerReportingServices' + } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -SetupConfiguration $mockSetupConfiguration } | Should -Throw -ErrorId 'GSRSCF0002*' + } + } + + Context 'When using Path parameter and the file exists' { + BeforeAll { + $mockConfigFilePath = 'C:\Backup\RSReportServer.config' + $mockConfigXmlContent = @' + + + BackupDsn + MSRS13.BACKUP + +'@ + + Mock -CommandName Test-Path -MockWith { + return $true + } -ParameterFilter { $Path -eq $mockConfigFilePath } + + Mock -CommandName Get-Content -MockWith { + return $mockConfigXmlContent + } + } + + It 'Should return the configuration file from the specified path' { + $result = Get-SqlDscRSConfigFile -Path $mockConfigFilePath + + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration.Dsn | Should -Be 'BackupDsn' + $result.Configuration.InstanceId | Should -Be 'MSRS13.BACKUP' + + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Content -Exactly -Times 1 -Scope It + } + } + + Context 'When using Path parameter and the file does not exist' { + BeforeAll { + $mockConfigFilePath = 'C:\NonExistent\RSReportServer.config' + + Mock -CommandName Test-Path -MockWith { + return $false + } -ParameterFilter { $Path -eq $mockConfigFilePath } + } + + It 'Should throw a terminating error' { + { Get-SqlDscRSConfigFile -Path $mockConfigFilePath } | Should -Throw -ErrorId 'GSRSCF0004*' + + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope It + } + } + + # cSpell: ignore PBIRS + Context 'When getting configuration file for Power BI Report Server' { + BeforeAll { + $mockConfigFilePath = 'C:\Program Files\PBIRS\PBIRS\ReportServer\RSReportServer.config' + $mockConfigXmlContent = @' + + + PBIRSDsn + PBIRS + + True + + +'@ + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'PBIRS' + InstallFolder = 'C:\Program Files\PBIRS' + ConfigFilePath = $mockConfigFilePath + ServiceName = 'PowerBIReportServer' + } + } + + Mock -CommandName Get-Content -MockWith { + return $mockConfigXmlContent + } + } + + It 'Should return the configuration file for PBIRS' { + $result = Get-SqlDscRSConfigFile -InstanceName 'PBIRS' + + $result | Should -BeOfType 'System.Xml.XmlDocument' + $result.Configuration.Dsn | Should -Be 'PBIRSDsn' + $result.Configuration.InstanceId | Should -Be 'PBIRS' + $result.Configuration.Service.IsDataModelRefreshService | Should -Be 'True' + + Should -Invoke -CommandName Get-SqlDscRSSetupConfiguration -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Content -Exactly -Times 1 -Scope It + } + } + + Context 'When using XPath queries on the returned XML' { + BeforeAll { + $mockConfigFilePath = 'C:\Program Files\SSRS\SSRS\ReportServer\RSReportServer.config' + $mockConfigXmlContent = @' + + + + + + + + + + + + + +'@ + + Mock -CommandName Get-SqlDscRSSetupConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + InstallFolder = 'C:\Program Files\SSRS' + ConfigFilePath = $mockConfigFilePath + ServiceName = 'SQLServerReportingServices' + } + } + + Mock -CommandName Get-Content -MockWith { + return $mockConfigXmlContent + } + } + + It 'Should support SelectSingleNode XPath queries' { + $result = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + + $pdfExtension = $result.SelectSingleNode('//Extension[@Name="PDF"]') + + $pdfExtension | Should -Not -BeNullOrEmpty + $pdfExtension.Name | Should -Be 'PDF' + } + + It 'Should support SelectNodes XPath queries' { + $result = Get-SqlDscRSConfigFile -InstanceName 'SSRS' + + $extensions = $result.SelectNodes('//Extension[@Name]') + + $extensions.Count | Should -Be 4 + ($extensions | ForEach-Object { $_.Name }) | Should -Contain 'PDF' + ($extensions | ForEach-Object { $_.Name }) | Should -Contain 'Report Server Email' + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSUrl.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSUrl.Tests.ps1 new file mode 100644 index 000000000..e09b71f1a --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSUrl.Tests.ps1 @@ -0,0 +1,391 @@ +[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-SqlDscRSUrl' { + BeforeAll { + InModuleScope -ScriptBlock { + function script:Get-CimInstance + { + param + ( + [System.String] + $ClassName, + + [System.String] + $Namespace, + + [System.String] + $Filter, + + [System.String] + $ErrorAction + ) + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + 'StubNotImplemented', + 'StubCalledError', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $MyInvocation.MyCommand + ) + ) + } + } + } + + AfterAll { + InModuleScope -ScriptBlock { + Remove-Item -Path 'function:script:Get-CimInstance' -Force + } + } + + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-SetupConfiguration] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSUrl').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 Report Server URLs successfully' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + $mockCimMethodResult = [PSCustomObject] @{ + ApplicationName = @('ReportServerWebService', 'ReportServerWebApp') + URLs = @('http://localhost:80/ReportServer', 'http://localhost:80/Reports') + } + + Mock -CommandName Get-CimInstance -MockWith { + return [PSCustomObject] @{ + InstanceId = 'SSRS' + } + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return $mockCimMethodResult + } + } + + It 'Should return Report Server URLs' { + $result = $mockSetupConfiguration | Get-SqlDscRSUrl + + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + + Should -Invoke -CommandName Get-CimInstance -ParameterFilter { + $Namespace -eq 'root\Microsoft\SqlServer\ReportServer\RS_SSRS\v15' -and + $ClassName -eq 'MSReportServer_Instance' + } -Exactly -Times 1 + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GetReportServerUrls' + } -Exactly -Times 1 + } + + It 'Should return ReportServerUri objects with correct properties' { + $result = $mockSetupConfiguration | Get-SqlDscRSUrl + + $result[0].InstanceName | Should -Be 'SSRS' + $result[0].ApplicationName | Should -Be 'ReportServerWebService' + $result[0].Uri | Should -Be 'http://localhost:80/ReportServer' + + $result[1].InstanceName | Should -Be 'SSRS' + $result[1].ApplicationName | Should -Be 'ReportServerWebApp' + $result[1].Uri | Should -Be 'http://localhost:80/Reports' + } + } + + Context 'When an application has multiple URLs' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + # When an application has multiple URLs, the ApplicationName appears multiple times + # in the array with corresponding URLs at the same index + $mockCimMethodResult = [PSCustomObject] @{ + ApplicationName = @('ReportServerWebService', 'ReportServerWebService') + URLs = @('http://localhost:80/ReportServer', 'https://localhost:443/ReportServer') + } + + Mock -CommandName Get-CimInstance -MockWith { + return [PSCustomObject] @{ + InstanceId = 'SSRS' + } + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return $mockCimMethodResult + } + } + + It 'Should return all URLs for the application' { + $result = $mockSetupConfiguration | Get-SqlDscRSUrl + + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + $result[0].Uri | Should -Be 'http://localhost:80/ReportServer' + $result[1].Uri | Should -Be 'https://localhost:443/ReportServer' + $result[0].ApplicationName | Should -Be 'ReportServerWebService' + $result[1].ApplicationName | Should -Be 'ReportServerWebService' + } + } + + Context 'When no URLs are configured' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + $mockCimMethodResult = [PSCustomObject] @{ + ApplicationName = @() + URLs = @() + } + + Mock -CommandName Get-CimInstance -MockWith { + return [PSCustomObject] @{ + InstanceId = 'SSRS' + } + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return $mockCimMethodResult + } + } + + It 'Should return $null' { + $result = $mockSetupConfiguration | Get-SqlDscRSUrl + + $result | Should -BeNullOrEmpty + } + } + + Context 'When CurrentVersion is null or empty' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = $null + } + } + + It 'Should throw a terminating error' { + { $mockSetupConfiguration | Get-SqlDscRSUrl } | Should -Throw -ErrorId 'GSRSU0001,Get-SqlDscRSUrl' + } + } + + Context 'When Get-CimInstance fails' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + Mock -CommandName Get-CimInstance -MockWith { + throw 'CIM instance not found' + } + } + + It 'Should throw a terminating error' { + { $mockSetupConfiguration | Get-SqlDscRSUrl } | Should -Throw -ErrorId 'GSRSU0002,Get-SqlDscRSUrl' + } + } + + Context 'When Get-CimInstance returns null' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + Mock -CommandName Get-CimInstance -MockWith { + return $null + } + } + + It 'Should throw a terminating error' { + { $mockSetupConfiguration | Get-SqlDscRSUrl } | Should -Throw -ErrorId 'GSRSU0003,Get-SqlDscRSUrl' + } + } + + Context 'When Invoke-RsCimMethod fails' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + Mock -CommandName Get-CimInstance -MockWith { + return [PSCustomObject] @{ + InstanceId = 'SSRS' + } + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method GetReportServerUrls() failed' + } + } + + It 'Should throw a terminating error' { + { $mockSetupConfiguration | Get-SqlDscRSUrl } | Should -Throw -ErrorId 'GSRSU0004,Get-SqlDscRSUrl' + } + } + + Context 'When passing SetupConfiguration as parameter' { + BeforeAll { + $mockSetupConfiguration = [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + } + + $mockCimMethodResult = [PSCustomObject] @{ + ApplicationName = @('ReportServerWebService') + URLs = @('http://localhost:80/ReportServer') + } + + Mock -CommandName Get-CimInstance -MockWith { + return [PSCustomObject] @{ + InstanceId = 'SSRS' + } + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return $mockCimMethodResult + } + } + + It 'Should get Report Server URLs' { + $result = Get-SqlDscRSUrl -SetupConfiguration $mockSetupConfiguration + + $result | Should -Not -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When processing multiple SetupConfigurations via pipeline' { + BeforeAll { + $mockSetupConfigurations = @( + [PSCustomObject] @{ + InstanceName = 'SSRS' + InstanceId = 'SSRS' + CurrentVersion = '15.0.1100.0' + }, + [PSCustomObject] @{ + InstanceName = 'PBIRS' + InstanceId = 'PBIRS' + CurrentVersion = '15.0.1100.0' + } + ) + + $script:callCount = 0 + + Mock -CommandName Get-CimInstance -MockWith { + return [PSCustomObject] @{ + InstanceId = $Filter -replace "InstanceId='([^']+)'", '$1' + } + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + $script:callCount++ + + if ($script:callCount -eq 1) + { + return [PSCustomObject] @{ + ApplicationName = @('ReportServerWebService') + URLs = @('http://localhost:80/ReportServer_SSRS') + } + } + else + { + return [PSCustomObject] @{ + ApplicationName = @('ReportServerWebService') + URLs = @('http://localhost:80/ReportServer_PBIRS') + } + } + } + } + + It 'Should return URLs for all instances' { + $result = $mockSetupConfigurations | Get-SqlDscRSUrl + + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -Be 2 + + $result[0].InstanceName | Should -Be 'SSRS' + $result[0].Uri | Should -Be 'http://localhost:80/ReportServer_SSRS' + + $result[1].InstanceName | Should -Be 'PBIRS' + $result[1].Uri | Should -Be 'http://localhost:80/ReportServer_PBIRS' + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 2 + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 2 + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 index 821bbde7e..8cf3c27fd 100644 --- a/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 @@ -48,7 +48,7 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { It 'Should have the correct parameters in parameter set ' -ForEach @( @{ ExpectedParameterSetName = '__AllParameterSets' - ExpectedParameters = '[-Configuration] []' + ExpectedParameters = '[-SetupConfiguration] []' } ) { $result = (Get-Command -Name 'Get-SqlDscRSWebPortalApplicationName').ParameterSets | @@ -73,11 +73,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { return [System.Version] '13.0.4001.0' } - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -Be 'ReportServerWebApp' } @@ -87,11 +87,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { return [System.Version] '14.0.600.0' } - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -Be 'ReportServerWebApp' } @@ -101,11 +101,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { return [System.Version] '15.0.1100.0' } - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -Be 'ReportServerWebApp' } @@ -115,11 +115,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { return [System.Version] '16.0.1000.0' } - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -Be 'ReportServerWebApp' } @@ -135,11 +135,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { return [System.Version] '12.0.4100.0' } - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -Be 'ReportManager' } @@ -149,11 +149,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { return [System.Version] '11.0.5000.0' } - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -Be 'ReportManager' } @@ -165,11 +165,11 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { } It 'Should return null when Get-SqlDscRSVersion returns null' { - $mockConfiguration = @{ + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = $mockConfiguration | Get-SqlDscRSWebPortalApplicationName + $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName $result | Should -BeNullOrEmpty } @@ -182,12 +182,12 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { } } - It 'Should return the correct application name when Configuration is passed as parameter' { - $mockConfiguration = @{ + It 'Should return the correct application name when SetupConfiguration is passed as parameter' { + $mockSetupConfiguration = @{ InstanceName = 'SSRS' } - $result = Get-SqlDscRSWebPortalApplicationName -Configuration $mockConfiguration + $result = Get-SqlDscRSWebPortalApplicationName -SetupConfiguration $mockSetupConfiguration $result | Should -Be 'ReportServerWebApp' } From 048038dbb4b4a13286b17b071cceb484ac07e47e Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 12:42:11 +0100 Subject: [PATCH 06/11] Remove redundant type checks for ReportServerUri in Get-SqlDscRSUrl integration tests --- source/Public/Get-SqlDscRSUrl.ps1 | 2 +- .../Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/source/Public/Get-SqlDscRSUrl.ps1 b/source/Public/Get-SqlDscRSUrl.ps1 index 2757120be..34f37c6b4 100644 --- a/source/Public/Get-SqlDscRSUrl.ps1 +++ b/source/Public/Get-SqlDscRSUrl.ps1 @@ -56,7 +56,7 @@ function Get-SqlDscRSUrl { [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] [CmdletBinding()] - [OutputType([ReportServerUri[]])] + [OutputType([System.Object[]])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true)] diff --git a/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 index 6f03301e1..1fdd7f82d 100644 --- a/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1 @@ -53,7 +53,6 @@ Describe 'Get-SqlDscRSUrl' { $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' $result | Should -Not -BeNullOrEmpty - $result[0] | Should -BeOfType 'ReportServerUri' $result[0].PSObject.Properties.Name | Should -Contain 'InstanceName' $result[0].PSObject.Properties.Name | Should -Contain 'ApplicationName' $result[0].PSObject.Properties.Name | Should -Contain 'Uri' @@ -111,7 +110,6 @@ Describe 'Get-SqlDscRSUrl' { $result = $script:setupConfiguration | Get-SqlDscRSUrl -ErrorAction 'Stop' $result | Should -Not -BeNullOrEmpty - $result[0] | Should -BeOfType 'ReportServerUri' $result[0].PSObject.Properties.Name | Should -Contain 'InstanceName' $result[0].PSObject.Properties.Name | Should -Contain 'ApplicationName' $result[0].PSObject.Properties.Name | Should -Contain 'Uri' From 410a2a67b4ef9086bc37d5ee443a3accd5661eb7 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 12:47:03 +0100 Subject: [PATCH 07/11] Fix parameter name in documentation for Get-SqlDscRSConfigFile and refactor tests for Get-SqlDscRSWebPortalApplicationName --- source/Public/Get-SqlDscRSConfigFile.ps1 | 2 +- ...SqlDscRSWebPortalApplicationName.Tests.ps1 | 88 +++++++------------ 2 files changed, 33 insertions(+), 57 deletions(-) diff --git a/source/Public/Get-SqlDscRSConfigFile.ps1 b/source/Public/Get-SqlDscRSConfigFile.ps1 index 7165ad1f9..153b16fdc 100644 --- a/source/Public/Get-SqlDscRSConfigFile.ps1 +++ b/source/Public/Get-SqlDscRSConfigFile.ps1 @@ -10,7 +10,7 @@ The configuration file path is automatically determined from the instance's setup configuration in the registry when using the - `InstanceName` or `Configuration` parameters. Alternatively, a direct + `InstanceName` or `SetupConfiguration` parameter. Alternatively, a direct file path can be specified using the `Path` parameter. .PARAMETER InstanceName diff --git a/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 index 8cf3c27fd..a32deb52d 100644 --- a/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscRSWebPortalApplicationName.Tests.ps1 @@ -68,51 +68,30 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { Mock -CommandName Get-SqlDscRSVersion } - It 'Should return ReportServerWebApp for SQL Server 2016 (version 13)' { - Mock -CommandName Get-SqlDscRSVersion -MockWith { - return [System.Version] '13.0.4001.0' - } - - $mockSetupConfiguration = @{ - InstanceName = 'SSRS' - } - - $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName - - $result | Should -Be 'ReportServerWebApp' - } - - It 'Should return ReportServerWebApp for SQL Server 2017 (version 14)' { - Mock -CommandName Get-SqlDscRSVersion -MockWith { - return [System.Version] '14.0.600.0' + It 'Should return for SQL Server (version )' -ForEach @( + @{ + Version = '13.0.4001.0' + VersionDescription = '2016' + ExpectedApplicationName = 'ReportServerWebApp' } - - $mockSetupConfiguration = @{ - InstanceName = 'SSRS' + @{ + Version = '14.0.600.0' + VersionDescription = '2017' + ExpectedApplicationName = 'ReportServerWebApp' } - - $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName - - $result | Should -Be 'ReportServerWebApp' - } - - It 'Should return ReportServerWebApp for SQL Server 2019 (version 15)' { - Mock -CommandName Get-SqlDscRSVersion -MockWith { - return [System.Version] '15.0.1100.0' + @{ + Version = '15.0.1100.0' + VersionDescription = '2019' + ExpectedApplicationName = 'ReportServerWebApp' } - - $mockSetupConfiguration = @{ - InstanceName = 'SSRS' + @{ + Version = '16.0.1000.0' + VersionDescription = '2022' + ExpectedApplicationName = 'ReportServerWebApp' } - - $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName - - $result | Should -Be 'ReportServerWebApp' - } - - It 'Should return ReportServerWebApp for SQL Server 2022 (version 16)' { + ) { Mock -CommandName Get-SqlDscRSVersion -MockWith { - return [System.Version] '16.0.1000.0' + return [System.Version] $Version } $mockSetupConfiguration = @{ @@ -121,7 +100,7 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName - $result | Should -Be 'ReportServerWebApp' + $result | Should -Be $ExpectedApplicationName } } @@ -130,23 +109,20 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { Mock -CommandName Get-SqlDscRSVersion } - It 'Should return ReportManager for SQL Server 2014 (version 12)' { - Mock -CommandName Get-SqlDscRSVersion -MockWith { - return [System.Version] '12.0.4100.0' + It 'Should return for SQL Server (version )' -ForEach @( + @{ + Version = '12.0.4100.0' + VersionDescription = '2014' + ExpectedApplicationName = 'ReportManager' } - - $mockSetupConfiguration = @{ - InstanceName = 'SSRS' + @{ + Version = '11.0.5000.0' + VersionDescription = '2012' + ExpectedApplicationName = 'ReportManager' } - - $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName - - $result | Should -Be 'ReportManager' - } - - It 'Should return ReportManager for SQL Server 2012 (version 11)' { + ) { Mock -CommandName Get-SqlDscRSVersion -MockWith { - return [System.Version] '11.0.5000.0' + return [System.Version] $Version } $mockSetupConfiguration = @{ @@ -155,7 +131,7 @@ Describe 'Get-SqlDscRSWebPortalApplicationName' { $result = $mockSetupConfiguration | Get-SqlDscRSWebPortalApplicationName - $result | Should -Be 'ReportManager' + $result | Should -Be $ExpectedApplicationName } } From b7a2f8148c91ce822499569bb060c7c7099a6642 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 12:49:45 +0100 Subject: [PATCH 08/11] Use -Raw parameter in Get-Content for improved performance in Get-SqlDscRSConfigFile function --- source/Public/Get-SqlDscRSConfigFile.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Public/Get-SqlDscRSConfigFile.ps1 b/source/Public/Get-SqlDscRSConfigFile.ps1 index 153b16fdc..ec936d0c1 100644 --- a/source/Public/Get-SqlDscRSConfigFile.ps1 +++ b/source/Public/Get-SqlDscRSConfigFile.ps1 @@ -193,7 +193,7 @@ function Get-SqlDscRSConfigFile try { - [xml] $configXml = Get-Content -Path $configFilePath -ErrorAction 'Stop' + [xml] $configXml = Get-Content -Path $configFilePath -Raw -ErrorAction 'Stop' } catch { From 4fac04d8ffcba996964cfd959c2b637a1ff5c37d Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 13:10:18 +0100 Subject: [PATCH 09/11] Add -Force parameter to Get-Content in Get-SqlDscRSConfigFile for improved error handling --- source/Public/Get-SqlDscRSConfigFile.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Public/Get-SqlDscRSConfigFile.ps1 b/source/Public/Get-SqlDscRSConfigFile.ps1 index ec936d0c1..6b82e363a 100644 --- a/source/Public/Get-SqlDscRSConfigFile.ps1 +++ b/source/Public/Get-SqlDscRSConfigFile.ps1 @@ -193,7 +193,7 @@ function Get-SqlDscRSConfigFile try { - [xml] $configXml = Get-Content -Path $configFilePath -Raw -ErrorAction 'Stop' + [xml] $configXml = Get-Content -Path $configFilePath -Raw -Force -ErrorAction 'Stop' } catch { From c8044c091270cce9b5b208f3cca18fb0daef4d4e Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 13:46:56 +0100 Subject: [PATCH 10/11] Add note for detailed trace log configuration in Troubleshooting Report Server guide --- source/WikiSource/Troubleshooting-Report-Server.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/WikiSource/Troubleshooting-Report-Server.md b/source/WikiSource/Troubleshooting-Report-Server.md index f60a2b3c5..b13eda654 100644 --- a/source/WikiSource/Troubleshooting-Report-Server.md +++ b/source/WikiSource/Troubleshooting-Report-Server.md @@ -25,6 +25,8 @@ are useful for diagnosing issues. > For comprehensive information about all available Reporting Services log > files and sources (including execution logs, trace logs, HTTP logs, and > performance logs), see [Reporting Services log files and sources](https://learn.microsoft.com/en-us/sql/reporting-services/report-server/reporting-services-log-files-and-sources). +> For detailed information about trace log configuration and content, see +> [Report Server Service Trace Log](https://learn.microsoft.com/en-us/sql/reporting-services/report-server/report-server-service-trace-log). ### Getting the Log Path From 9d3410d967e155ef48136369d5708310f4c5a631 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sat, 10 Jan 2026 14:20:33 +0100 Subject: [PATCH 11/11] Update test description for Get-SqlDscRSConfigFile to clarify piped SetupConfiguration behavior --- tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 index d47c62ff9..c2aa427be 100644 --- a/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 +++ b/tests/Unit/Public/Get-SqlDscRSConfigFile.Tests.ps1 @@ -240,7 +240,7 @@ Describe 'Get-SqlDscRSConfigFile' { } } - It 'Should return the configuration file when piping SetupConfiguration' { + It 'Should return the configuration file for piped SetupConfiguration' { $result = $mockSetupConfiguration | Get-SqlDscRSConfigFile $result | Should -BeOfType 'System.Xml.XmlDocument'