diff --git a/CHANGELOG.md b/CHANGELOG.md index e3c072dfff..5d220cd5c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added public command `Get-SqlDscRSPackage` to retrieve package information for + SQL Server Reporting Services or Power BI Report Server. Supports getting version + information from an executable file + ([issue #2082](https://github.com/dsccommunity/SqlServerDsc/issues/2082)). - Added public command `Get-SqlDscBackupFileList` to read the list of database files contained in a SQL Server backup file. Useful for planning file relocations during restore operations ([issue #2026](https://github.com/dsccommunity/SqlServerDsc/issues/2026)). diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 6f7c959f1d..e40af38e68 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -531,6 +531,7 @@ stages: 'tests/Integration/Commands/Install-SqlDscReportingService.Integration.Tests.ps1' # Group 2 'tests/Integration/Commands/Get-SqlDscInstalledInstance.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSSetupConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInstalled.Integration.Tests.ps1' # Group 8 @@ -596,6 +597,7 @@ stages: 'tests/Integration/Commands/Install-SqlDscPowerBIReportServer.Integration.Tests.ps1' # Group 2 'tests/Integration/Commands/Get-SqlDscInstalledInstance.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSSetupConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInstalled.Integration.Tests.ps1' # Group 8 diff --git a/source/Public/Get-SqlDscRSPackage.ps1 b/source/Public/Get-SqlDscRSPackage.ps1 new file mode 100644 index 0000000000..72e772499f --- /dev/null +++ b/source/Public/Get-SqlDscRSPackage.ps1 @@ -0,0 +1,92 @@ +<# + .SYNOPSIS + Gets package information for SQL Server Reporting Services or Power BI + Report Server. + + .DESCRIPTION + Gets package information for a SQL Server Reporting Services or Power BI + Report Server executable file. The command returns file version information + including product name, product version, file version, and other + version-related metadata. + + .PARAMETER FilePath + Specifies the path to the executable file to return version information for. + The file must have a product name matching either 'Microsoft SQL Server + Reporting Services' or 'Microsoft Power BI Report Server'. + + .PARAMETER Force + If specified, the ProductName validation is skipped. This allows retrieving + version information for executables with different product names. + + .EXAMPLE + Get-SqlDscRSPackage -FilePath 'E:\SQLServerReportingServices.exe' + + Returns package information from the specified SQL Server Reporting Services + executable file. + + .EXAMPLE + Get-SqlDscRSPackage -FilePath 'E:\PBIReportServer.exe' + + Returns package information from the specified Power BI Report Server + executable file. + + .EXAMPLE + Get-SqlDscRSPackage -FilePath 'E:\CustomReportServer.exe' -Force + + Returns package information from the specified executable file without + validating the product name. + + .INPUTS + None. + + .OUTPUTS + `System.Diagnostics.FileVersionInfo` + + Returns the file version information for the package. +#> +function Get-SqlDscRSPackage +{ + # cSpell: ignore PBIRS + [CmdletBinding()] + [OutputType([System.Diagnostics.FileVersionInfo])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $FilePath, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + $validProductNames = @( + 'Microsoft SQL Server Reporting Services' + 'Microsoft Power BI Report Server' + ) + + Write-Debug -Message ($script:localizedData.Get_SqlDscRSPackage_GettingVersionFromFile -f $FilePath) + + $versionInfo = Get-FileVersion -Path $FilePath + + if (-not $Force.IsPresent) + { + if ($versionInfo.ProductName -notin $validProductNames) + { + $errorMessage = $script:localizedData.Get_SqlDscRSPackage_InvalidProductName -f $versionInfo.ProductName, ($validProductNames -join "', '") + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + $errorMessage, + 'GSDRSP0002', + [System.Management.Automation.ErrorCategory]::InvalidArgument, + $FilePath + ) + ) + } + } + + Write-Debug -Message ($script:localizedData.Get_SqlDscRSPackage_ReturningVersionInfo -f $versionInfo.ProductName, $versionInfo.ProductVersion) + + return $versionInfo +} diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 881a5df70c..74dcd36d3d 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -665,4 +665,9 @@ ConvertFrom-StringData @' ## Get-SqlDscDateTime Get_SqlDscDateTime_RetrievingDateTime = Retrieving date and time using {0}(). (GSDD0001) Get_SqlDscDateTime_FailedToRetrieve = Failed to retrieve date and time using {0}(): {1} (GSDD0002) + + ## Get-SqlDscRSPackage + Get_SqlDscRSPackage_GettingVersionFromFile = Getting version information from file '{0}'. (GSDRSP0001) + Get_SqlDscRSPackage_InvalidProductName = The product name '{0}' is not a valid Reporting Services package. Expected product names are: '{1}'. Use the Force parameter to skip this validation. (GSDRSP0002) + Get_SqlDscRSPackage_ReturningVersionInfo = Returning version information for '{0}' version '{1}'. (GSDRSP0003) '@ diff --git a/tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1 new file mode 100644 index 0000000000..db1d85cb6b --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1 @@ -0,0 +1,99 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' +} + +Describe 'Get-SqlDscRSPackage' { + Context 'When getting package information for a non-existing file' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + It 'Should throw an error when the file does not exist' { + { Get-SqlDscRSPackage -FilePath 'C:\NonExistent\SQLServerReportingServices.exe' -ErrorAction 'Stop' } | Should -Throw + } + } + + Context 'When getting package information for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { + BeforeAll { + $script:temporaryFolder = Get-TemporaryFolder + $script:reportingServicesExecutable = Join-Path -Path $script:temporaryFolder -ChildPath 'SQLServerReportingServices.exe' + } + + It 'Should return the package information for SSRS' { + $result = Get-SqlDscRSPackage -FilePath $script:reportingServicesExecutable -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.ProductName | Should -Be 'Microsoft SQL Server Reporting Services' + $result.FileVersion | Should -Not -BeNullOrEmpty + $result.ProductVersion | Should -Not -BeNullOrEmpty + } + } + + # cSpell: ignore PBIRS + Context 'When getting package information for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:temporaryFolder = Get-TemporaryFolder + $script:powerBIReportServerExecutable = Join-Path -Path $script:temporaryFolder -ChildPath 'PowerBIReportServer.exe' + } + + It 'Should return the package information for PBIRS' { + $result = Get-SqlDscRSPackage -FilePath $script:powerBIReportServerExecutable -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.ProductName | Should -Be 'Microsoft Power BI Report Server' + $result.FileVersion | Should -Not -BeNullOrEmpty + $result.ProductVersion | Should -Not -BeNullOrEmpty + } + } + + Context 'When file has an invalid product name' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + It 'Should throw an error without Force parameter' { + # Use an executable that exists but has a different product name + { Get-SqlDscRSPackage -FilePath 'C:\Windows\System32\notepad.exe' -ErrorAction 'Stop' } | Should -Throw + } + + It 'Should return version information with Force parameter' { + $result = Get-SqlDscRSPackage -FilePath 'C:\Windows\System32\notepad.exe' -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.FileVersion | Should -Not -BeNullOrEmpty + } + } + + Context 'When using Force parameter to bypass validation' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { + BeforeAll { + $script:temporaryFolder = Get-TemporaryFolder + $script:reportingServicesExecutable = Join-Path -Path $script:temporaryFolder -ChildPath 'SQLServerReportingServices.exe' + } + + It 'Should return file version information with Force parameter' { + $result = Get-SqlDscRSPackage -FilePath $script:reportingServicesExecutable -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.FileVersion | Should -Not -BeNullOrEmpty + } + } +} diff --git a/tests/Integration/Commands/README.md b/tests/Integration/Commands/README.md index 5405aed133..71adea6370 100644 --- a/tests/Integration/Commands/README.md +++ b/tests/Integration/Commands/README.md @@ -162,6 +162,7 @@ Save-SqlDscSqlServerMediaFile | 0 | - | - | Downloads SQL Server media files Import-SqlDscPreferredModule | 0 | - | - | - Install-SqlDscReportingService | 1 | 0 (Prerequisites) | - | SSRS instance Get-SqlDscInstalledInstance | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Get-SqlDscRSPackage | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Get-SqlDscRSSetupConfiguration | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Test-SqlDscRSInstalled | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Repair-SqlDscReportingService | 8 | 1 (Install-SqlDscReportingService) | SSRS | - @@ -180,6 +181,7 @@ Save-SqlDscSqlServerMediaFile | 0 | - | - | Downloads SQL Server media files Import-SqlDscPreferredModule | 0 | - | - | - Install-SqlDscPowerBIReportServer | 1 | 0 (Prerequisites) | - | PBIRS instance Get-SqlDscInstalledInstance | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Get-SqlDscRSPackage | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Get-SqlDscRSSetupConfiguration | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Test-SqlDscRSInstalled | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Repair-SqlDscPowerBIReportServer | 8 | 1 (Install-SqlDscPowerBIReportServer) | PBIRS | - diff --git a/tests/QA/ScriptAnalyzer.Tests.ps1 b/tests/QA/ScriptAnalyzer.Tests.ps1 index b2211ce864..1c05164be0 100644 --- a/tests/QA/ScriptAnalyzer.Tests.ps1 +++ b/tests/QA/ScriptAnalyzer.Tests.ps1 @@ -39,7 +39,27 @@ BeforeDiscovery { (from Indented.ScriptAnalyzerRules) can properly parse parameters that uses SMO types, e.g. [Microsoft.SqlServer.Management.Smo.Server]. #> - Add-Type -Path "$PSScriptRoot/../Unit/Stubs/SMO.cs" -ReferencedAssemblies 'System.Data', 'System.Xml' + if ($IsLinux -or $IsMacOS) + { + # .NET Core requires different assemblies than .NET Framework + $referencedAssemblies = @( + 'System.Collections' + 'System.Collections.Specialized' + 'System.Data.Common' + 'System.Linq' + 'System.Net.Primitives' + 'netstandard' + ) + } + else + { + $referencedAssemblies = @( + 'System.Data' + 'System.Xml' + ) + } + + Add-Type -Path "$PSScriptRoot/../Unit/Stubs/SMO.cs" -ReferencedAssemblies $referencedAssemblies $repositoryPath = Resolve-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../..') $sourcePath = Join-Path -Path $repositoryPath -ChildPath 'source' diff --git a/tests/Unit/Public/Get-SqlDscRSPackage.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSPackage.Tests.ps1 new file mode 100644 index 0000000000..e2ffeb0a37 --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSPackage.Tests.ps1 @@ -0,0 +1,200 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:moduleName -All | Remove-Module -Force + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Get-SqlDscRSPackage' -Tag 'Public' { + Context 'When using the FilePath parameter' { + Context 'When the file is a valid SSRS executable' { + BeforeAll { + # Create a mock file in TestDrive + $script:mockFilePath = Join-Path -Path $TestDrive -ChildPath 'SQLServerReportingServices.exe' + $null = New-Item -Path $script:mockFilePath -ItemType File -Force + + Mock -CommandName Get-FileVersion -MockWith { + return [PSCustomObject]@{ + ProductName = 'Microsoft SQL Server Reporting Services' + ProductVersion = '15.0.8963.8162' + FileVersion = '2019.150.8963.8162' + FileName = $script:mockFilePath + } + } + } + + It 'Should return the version information' { + $result = Get-SqlDscRSPackage -FilePath $script:mockFilePath + + $result | Should -Not -BeNullOrEmpty + $result.ProductName | Should -Be 'Microsoft SQL Server Reporting Services' + $result.ProductVersion | Should -Be '15.0.8963.8162' + + Should -Invoke -CommandName Get-FileVersion -Exactly -Times 1 -Scope It + } + } + + Context 'When the file is a valid PBIRS executable' { + BeforeAll { + # Create a mock file in TestDrive + $script:mockFilePath = Join-Path -Path $TestDrive -ChildPath 'PBIReportServer.exe' + $null = New-Item -Path $script:mockFilePath -ItemType File -Force + + Mock -CommandName Get-FileVersion -MockWith { + return [PSCustomObject]@{ + ProductName = 'Microsoft Power BI Report Server' + ProductVersion = '15.0.1111.1234' + FileVersion = '2019.150.1111.1234' + FileName = $script:mockFilePath + } + } + } + + It 'Should return the version information' { + $result = Get-SqlDscRSPackage -FilePath $script:mockFilePath + + $result | Should -Not -BeNullOrEmpty + $result.ProductName | Should -Be 'Microsoft Power BI Report Server' + $result.ProductVersion | Should -Be '15.0.1111.1234' + + Should -Invoke -CommandName Get-FileVersion -Exactly -Times 1 -Scope It + } + } + + Context 'When the file has an invalid product name' { + BeforeAll { + # Create a mock file in TestDrive + $script:mockFilePath = Join-Path -Path $TestDrive -ChildPath 'SomeProduct.exe' + $null = New-Item -Path $script:mockFilePath -ItemType File -Force + + Mock -CommandName Get-FileVersion -MockWith { + return [PSCustomObject]@{ + ProductName = 'Some Other Product' + ProductVersion = '1.0.0.0' + FileVersion = '1.0.0.0' + FileName = $script:mockFilePath + } + } + } + + It 'Should throw an error' { + $mockErrorMessage = InModuleScope -ScriptBlock { + $validProductNames = @( + 'Microsoft SQL Server Reporting Services' + 'Microsoft Power BI Report Server' + ) + + $script:localizedData.Get_SqlDscRSPackage_InvalidProductName -f 'Some Other Product', ($validProductNames -join "', '") + } + + { Get-SqlDscRSPackage -FilePath $script:mockFilePath } | Should -Throw -ExpectedMessage $mockErrorMessage + } + } + + Context 'When the file has an invalid product name but Force is specified' { + BeforeAll { + # Create a mock file in TestDrive + $script:mockFilePath = Join-Path -Path $TestDrive -ChildPath 'SomeProduct2.exe' + $null = New-Item -Path $script:mockFilePath -ItemType File -Force + + Mock -CommandName Get-FileVersion -MockWith { + return [PSCustomObject]@{ + ProductName = 'Some Other Product' + ProductVersion = '1.0.0.0' + FileVersion = '1.0.0.0' + FileName = $script:mockFilePath + } + } + } + + It 'Should return the version information without throwing' { + $result = Get-SqlDscRSPackage -FilePath $script:mockFilePath -Force + + $result | Should -Not -BeNullOrEmpty + $result.ProductName | Should -Be 'Some Other Product' + $result.ProductVersion | Should -Be '1.0.0.0' + + Should -Invoke -CommandName Get-FileVersion -Exactly -Times 1 -Scope It + } + } + } + + Context 'Parameter validation' { + It 'Should have the correct parameters in parameter set __AllParameterSets' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-FilePath] [-Force] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSPackage').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + + It 'Should have the correct parameters' { + $commandInfo = Get-Command -Name 'Get-SqlDscRSPackage' + + $commandInfo.Parameters['FilePath'] | Should -Not -BeNullOrEmpty + $commandInfo.Parameters['Force'] | Should -Not -BeNullOrEmpty + } + + It 'Should have FilePath as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSPackage').Parameters['FilePath'] + $allParameterSets = $parameterInfo.ParameterSets['__AllParameterSets'] + $allParameterSets.IsMandatory | Should -BeTrue + } + + It 'Should have Force as a non-mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSPackage').Parameters['Force'] + $allParameterSets = $parameterInfo.ParameterSets['__AllParameterSets'] + $allParameterSets.IsMandatory | Should -BeFalse + } + } +}