diff --git a/.vscode/settings.json b/.vscode/settings.json index 149e8cf0d..7c025cccb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -109,7 +109,8 @@ "hotfixes", "checkpointing", "HRESULT", - "RSDB" + "RSDB", + "RSIP" ], "cSpell.ignorePaths": [ ".git" diff --git a/CHANGELOG.md b/CHANGELOG.md index f0037225d..33bb9da3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ErrorDumpDirectory from the instance's setup configuration, which can be used with `Get-ChildItem` and `Get-Content` to access service logs, portal logs, and memory dumps. +- Added public command `Get-SqlDscRSExecutionLog` to query execution log entries + from the `ExecutionLog3` view in the report server database. Supports filtering + by date range, user name, report path, and maximum rows. Includes connection + parameters **Credential**, **LoginType**, **Encrypt**, and **StatementTimeout**. - Added public command `Test-SqlDscRSAccessible` to verify that SQL Server Reporting Services or Power BI Report Server web sites are accessible. Supports both CIM configuration input (with dynamic `-Site` parameter) and @@ -233,6 +237,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 by calling the `InitializeReportServer` CIM method. Used to complete initial configuration after database and URL setup ([issue #2014](https://github.com/dsccommunity/SqlServerDsc/issues/2014)). +- Added public command `Get-SqlDscRSIPAddress` to list IP addresses available + for URL reservations. Wraps the `ListIPAddresses` CIM method. +- Added public command `Get-SqlDscRSDatabaseInstallation` to determine whether + a specific report server database is a Reporting Services database. Wraps + the `ListReportServersInDatabase` CIM method. - Added public command `Request-SqlDscRSDatabaseUpgradeScript` to generate a T-SQL script for upgrading the report server database schema. Wraps the `GenerateDatabaseUpgradeScript` CIM method. diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2ab5ed22f..6d97faac4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -586,6 +586,9 @@ stages: # Group 5 - Post-initialization validation 'tests/Integration/Commands/Post.Initialization.RS.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSIPAddress.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSDatabaseInstallation.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSExecutionLog.Integration.Tests.ps1' # Group 6 - Service account change 'tests/Integration/Commands/Set-SqlDscRSServiceAccount.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSServiceAccount.Integration.Tests.ps1' @@ -683,6 +686,9 @@ stages: # Group 5 - Post-initialization validation 'tests/Integration/Commands/Post.Initialization.RS.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSUrl.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSIPAddress.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSDatabaseInstallation.Integration.Tests.ps1' + 'tests/Integration/Commands/Get-SqlDscRSExecutionLog.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.ReportServerIPAddress.ps1 b/source/Classes/002.ReportServerIPAddress.ps1 new file mode 100644 index 000000000..eaf86ef00 --- /dev/null +++ b/source/Classes/002.ReportServerIPAddress.ps1 @@ -0,0 +1,46 @@ +<# + .SYNOPSIS + Represents an IP address entry returned by the ListIPAddresses CIM method. + + .DESCRIPTION + This class represents an IP address available on the report server machine, + including the IP version (V4 or V6) and whether it is DHCP-enabled. + + .PARAMETER IPAddress + The IP address string. + + .PARAMETER IPVersion + The version of the IP address. Values are 'V4' for IPv4 or 'V6' for IPv6. + + .PARAMETER IsDhcpEnabled + Indicates whether the IP address is DHCP-enabled. If true, the IP address + is dynamic and should not be used for TLS bindings. + + .EXAMPLE + [ReportServerIPAddress]::new() + + Creates a new empty ReportServerIPAddress instance. + + .EXAMPLE + $ipAddress = [ReportServerIPAddress]::new() + $ipAddress.IPAddress = '192.168.1.1' + $ipAddress.IPVersion = 'V4' + $ipAddress.IsDhcpEnabled = $false + + Creates a new ReportServerIPAddress instance with property values. +#> +class ReportServerIPAddress +{ + [System.String] + $IPAddress + + [System.String] + $IPVersion + + [System.Boolean] + $IsDhcpEnabled + + ReportServerIPAddress() + { + } +} diff --git a/source/Public/Get-SqlDscRSDatabaseInstallation.ps1 b/source/Public/Get-SqlDscRSDatabaseInstallation.ps1 new file mode 100644 index 000000000..f952cd79e --- /dev/null +++ b/source/Public/Get-SqlDscRSDatabaseInstallation.ps1 @@ -0,0 +1,112 @@ +<# + .SYNOPSIS + Gets the report server installations registered in the database. + + .DESCRIPTION + Gets the Reporting Services installations registered in the report + server database by calling the `ListReportServersInDatabase` method + on the `MSReportServer_ConfigurationSetting` CIM instance. + + This command returns information about all report server installations + that are configured to use the same report server database, which is + useful in scale-out deployment scenarios. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Get-SqlDscRSDatabaseInstallation + + Gets all report server installations registered in the database. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Get-SqlDscRSDatabaseInstallation -Configuration $config + + Gets report server installations using a stored configuration object. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + `System.Management.Automation.PSCustomObject` + + Returns objects with properties: InstallationID, MachineName, + InstanceName, and IsInitialized. + + .NOTES + This command calls the CIM/WMI provider method `ListReportServersInDatabase` + using `Invoke-RsCimMethod`. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-listreportserversindatabase +#> +function Get-SqlDscRSDatabaseInstallation +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding()] + [OutputType([System.Management.Automation.PSCustomObject])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration + ) + + process + { + $instanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSDatabaseInstallation_Getting -f $instanceName) + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'ListReportServersInDatabase' + } + + try + { + $result = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + + <# + The WMI method returns: + - Length: Number of entries in the arrays + - InstallationID: Array of installation IDs + - MachineName: Array of machine names + - InstanceName: Array of instance names + - IsInitialized: Array of initialization states + #> + if ($result.Length -gt 0) + { + for ($i = 0; $i -lt $result.Length; $i++) + { + [PSCustomObject] @{ + InstallationID = $result.InstallationIDs[$i] + MachineName = $result.MachineNames[$i] + InstanceName = $result.InstanceNames[$i] + IsInitialized = $result.IsInitialized[$i] + } + } + } + } + catch + { + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + ($script:localizedData.Get_SqlDscRSDatabaseInstallation_FailedToGet -f $instanceName, $_.Exception.Message), + 'GSRSDI0001', + [System.Management.Automation.ErrorCategory]::InvalidOperation, + $Configuration + ) + ) + } + } +} diff --git a/source/Public/Get-SqlDscRSExecutionLog.ps1 b/source/Public/Get-SqlDscRSExecutionLog.ps1 new file mode 100644 index 000000000..2e908aa9a --- /dev/null +++ b/source/Public/Get-SqlDscRSExecutionLog.ps1 @@ -0,0 +1,308 @@ +<# + .SYNOPSIS + Gets execution log entries from the SQL Server Reporting Services or + Power BI Report Server database. + + .DESCRIPTION + Gets execution log entries from the `ExecutionLog3` view in the report + server database for SQL Server Reporting Services (SSRS) or Power BI + Report Server (PBIRS). This view provides detailed records of report + executions, including users, execution times, parameters, and rendering + data. + + The command automatically retrieves the database connection information + (server name and database name) from the Reporting Services configuration + CIM instance. + + The `ExecutionLog3` view is available in SQL Server 2016 and later versions. + + .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 is a mandatory parameter. + + .PARAMETER StartTime + Specifies the start time to filter execution log entries. Only entries + with a TimeStart greater than or equal to this value will be returned. + + .PARAMETER EndTime + Specifies the end time to filter execution log entries. Only entries + with a TimeStart less than or equal to this value will be returned. + + .PARAMETER UserName + Specifies the user name to filter execution log entries. Supports SQL + LIKE pattern matching (e.g., 'DOMAIN\%' or '%admin%'). + + .PARAMETER ReportPath + Specifies the report path to filter execution log entries. Supports SQL + LIKE pattern matching (e.g., '/Sales/%' or '%Revenue%'). + + .PARAMETER MaxRows + Specifies the maximum number of rows to return. Defaults to 1000. + Set to 0 to return all rows (use with caution on large databases). + + .PARAMETER Credential + Specifies the credentials to use to impersonate a user when connecting + to the report server database. If not provided, the current user's + Windows credentials will be used. + + .PARAMETER LoginType + Specifies which type of credentials are specified. The valid types are + Integrated, WindowsUser, and SqlLogin. If WindowsUser or SqlLogin are + specified then the Credential needs to be specified as well. Defaults + to 'Integrated'. + + .PARAMETER Encrypt + Specifies whether encryption should be used for the database connection. + + .PARAMETER StatementTimeout + Specifies the query statement timeout in seconds. Default is 600 seconds + (10 minutes). + + .PARAMETER Force + Specifies that the query should be executed without any confirmation. + + .EXAMPLE + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Force + + Returns the last 1000 execution log entries from the SSRS report server + database. + + .EXAMPLE + Get-SqlDscRSExecutionLog -InstanceName 'PBIRS' -MaxRows 100 -Force + + Returns the last 100 execution log entries from the Power BI Report + Server database. + + .EXAMPLE + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -StartTime (Get-Date).AddDays(-7) -Force + + Returns execution log entries from the last 7 days. + + .EXAMPLE + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -UserName 'DOMAIN\%' -Force + + Returns execution log entries for all users in the DOMAIN domain. + + .EXAMPLE + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -ReportPath '/Sales/%' -MaxRows 500 -Force + + Returns up to 500 execution log entries for reports in the /Sales/ folder. + + .EXAMPLE + $cred = Get-Credential + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Credential $cred -LoginType 'SqlLogin' -Force + + Returns execution log entries using SQL Server authentication. + + .INPUTS + None. + + .OUTPUTS + `System.Data.DataRow` + + Returns DataRow objects containing execution log entries with columns + such as InstanceName, ItemPath, UserName, ExecutionId, RequestType, + Format, Parameters, ItemAction, TimeStart, TimeEnd, TimeDataRetrieval, + TimeProcessing, TimeRendering, Source, Status, ByteCount, RowCount, + and AdditionalInfo. +#> +function Get-SqlDscRSExecutionLog +{ + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + [OutputType([System.Data.DataRow])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter()] + [System.DateTime] + $StartTime, + + [Parameter()] + [System.DateTime] + $EndTime, + + [Parameter()] + [System.String] + $UserName, + + [Parameter()] + [System.String] + $ReportPath, + + [Parameter()] + [ValidateRange(0, [System.Int32]::MaxValue)] + [System.Int32] + $MaxRows = 1000, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [ValidateSet('Integrated', 'WindowsUser', 'SqlLogin')] + [System.String] + $LoginType = 'Integrated', + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Encrypt, + + [Parameter()] + [ValidateNotNull()] + [System.Int32] + $StatementTimeout = 600, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSExecutionLog_GettingConfiguration -f $InstanceName) + + $rsConfiguration = Get-SqlDscRSConfiguration -InstanceName $InstanceName + + $databaseServerName = $rsConfiguration.DatabaseServerName + $databaseName = $rsConfiguration.DatabaseName + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSExecutionLog_DatabaseInfo -f $databaseName, $databaseServerName) + + # Parse the database server name to extract server and instance + $serverName = $databaseServerName + $sqlInstanceName = 'MSSQLSERVER' + + if ($databaseServerName -match '^(?[^\\]+)\\(?.+)$') + { + $serverName = $Matches['server'] + $sqlInstanceName = $Matches['instance'] + } + + # Build the WHERE clause based on filter parameters + $whereConditions = @() + + if ($PSBoundParameters.ContainsKey('StartTime')) + { + $startTimeString = $StartTime.ToString('yyyy-MM-dd HH:mm:ss') + $whereConditions += "TimeStart >= '$startTimeString'" + } + + if ($PSBoundParameters.ContainsKey('EndTime')) + { + $endTimeString = $EndTime.ToString('yyyy-MM-dd HH:mm:ss') + $whereConditions += "TimeStart <= '$endTimeString'" + } + + if ($PSBoundParameters.ContainsKey('UserName')) + { + $escapedUserName = $UserName -replace "'", "''" + $whereConditions += "UserName LIKE '$escapedUserName'" + } + + if ($PSBoundParameters.ContainsKey('ReportPath')) + { + $escapedReportPath = $ReportPath -replace "'", "''" + $whereConditions += "ItemPath LIKE '$escapedReportPath'" + } + + # Build the query + $topClause = '' + + if ($MaxRows -gt 0) + { + $topClause = "TOP ($MaxRows)" + } + + $whereClause = '' + + if ($whereConditions.Count -gt 0) + { + $whereClause = 'WHERE ' + ($whereConditions -join ' AND ') + } + + $query = @" +SELECT $topClause + InstanceName, + ItemPath, + UserName, + ExecutionId, + RequestType, + Format, + Parameters, + ItemAction, + TimeStart, + TimeEnd, + TimeDataRetrieval, + TimeProcessing, + TimeRendering, + Source, + Status, + ByteCount, + [RowCount], + AdditionalInfo +FROM ExecutionLog3 +$whereClause +ORDER BY TimeStart DESC +"@ + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSExecutionLog_ExecutingQuery -f $databaseName) + + $verboseDescriptionMessage = $script:localizedData.Get_SqlDscRSExecutionLog_ShouldProcessDescription -f $databaseName, $databaseServerName + $verboseWarningMessage = $script:localizedData.Get_SqlDscRSExecutionLog_ShouldProcessConfirmation -f $databaseName + $captionMessage = $script:localizedData.Get_SqlDscRSExecutionLog_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($verboseDescriptionMessage, $verboseWarningMessage, $captionMessage)) + { + $invokeSqlDscQueryParameters = @{ + ServerName = $serverName + InstanceName = $sqlInstanceName + DatabaseName = $databaseName + Query = $query + PassThru = $true + StatementTimeout = $StatementTimeout + Force = $true + ErrorAction = 'Stop' + Verbose = $VerbosePreference + } + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $invokeSqlDscQueryParameters.Credential = $Credential + } + + if ($LoginType -ne 'Integrated') + { + $invokeSqlDscQueryParameters.LoginType = $LoginType + } + + if ($Encrypt.IsPresent) + { + $invokeSqlDscQueryParameters.Encrypt = $true + } + + try + { + $result = Invoke-SqlDscQuery @invokeSqlDscQueryParameters + + if ($result -and $result.Tables -and $result.Tables[0].Rows) + { + return $result.Tables[0].Rows + } + } + catch + { + $errorMessage = $script:localizedData.Get_SqlDscRSExecutionLog_QueryFailed -f $InstanceName, $_.Exception.Message + + Write-Error -Message $errorMessage -Category 'InvalidOperation' -ErrorId 'GSRSEL0001' -TargetObject $InstanceName -Exception $_.Exception + + return + } + } +} diff --git a/source/Public/Get-SqlDscRSIPAddress.ps1 b/source/Public/Get-SqlDscRSIPAddress.ps1 new file mode 100644 index 000000000..abfaa3445 --- /dev/null +++ b/source/Public/Get-SqlDscRSIPAddress.ps1 @@ -0,0 +1,106 @@ +<# + .SYNOPSIS + Gets the available IP addresses for SQL Server Reporting Services. + + .DESCRIPTION + Gets the IP addresses available on the machine for use with + SQL Server Reporting Services or Power BI Report Server by calling + the `ListIPAddresses` method on the + `MSReportServer_ConfigurationSetting` CIM instance. + + This command returns information about IP addresses that can be + used for URL reservations and SSL bindings. Each returned object + includes the IP address, IP version (V4 or V6), and whether DHCP + is enabled. If DHCP is enabled, the IP address is dynamic and should + not be used for TLS bindings. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Get-SqlDscRSIPAddress + + Gets all available IP addresses for the Reporting Services instance. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Get-SqlDscRSIPAddress -Configuration $config | Where-Object -FilterScript { $_.IPVersion -eq 'V4' } + + Gets available IPv4 addresses for the Reporting Services instance. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + `[ReportServerIPAddress[]]` + + Returns an array of ReportServerIPAddress objects containing the IP + address, IP version, and DHCP status. + + .NOTES + This command calls the WMI method `ListIPAddresses`. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-listipaddresses +#> +function Get-SqlDscRSIPAddress +{ + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding()] + [OutputType([ReportServerIPAddress[]])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration + ) + + process + { + $instanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Get_SqlDscRSIPAddress_Getting -f $instanceName) + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'ListIPAddresses' + } + + try + { + $result = Invoke-RsCimMethod @invokeRsCimMethodParameters + + # Return the IP addresses as ReportServerIPAddress objects + if ($result.IPAddress -and $result.IPAddress.Count -gt 0) + { + $ipAddressObjects = for ($i = 0; $i -lt $result.IPAddress.Count; $i++) + { + $ipAddressObject = [ReportServerIPAddress]::new() + $ipAddressObject.IPAddress = $result.IPAddress[$i] + $ipAddressObject.IPVersion = $result.IPVersion[$i] + $ipAddressObject.IsDhcpEnabled = $result.IsDhcpEnabled[$i] + + $ipAddressObject + } + + return $ipAddressObjects + } + } + catch + { + $errorMessage = $script:localizedData.Get_SqlDscRSIPAddress_FailedToGet -f $instanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'GSRSIP0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } +} diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index 7338fb38b..c0942bf40 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -319,6 +319,16 @@ ConvertFrom-StringData @' 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-SqlDscRSExecutionLog + Get_SqlDscRSExecutionLog_GettingConfiguration = Getting Reporting Services configuration for instance '{0}'. + Get_SqlDscRSExecutionLog_DatabaseInfo = Report server database '{0}' on server '{1}'. + Get_SqlDscRSExecutionLog_ExecutingQuery = Executing query against database '{0}'. + Get_SqlDscRSExecutionLog_ShouldProcessDescription = Querying execution log from database '{0}' on server '{1}'. + Get_SqlDscRSExecutionLog_ShouldProcessConfirmation = Are you sure you want to query the execution log from database '{0}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Get_SqlDscRSExecutionLog_ShouldProcessCaption = Query Reporting Services execution log + Get_SqlDscRSExecutionLog_QueryFailed = Failed to query execution log for Reporting Services instance '{0}'. {1} (GSRSEL0001) + ## Get-SqlDscRSWebPortalApplicationName Get_SqlDscRSWebPortalApplicationName_GettingApplicationName = Getting web portal application name. @@ -871,6 +881,14 @@ ConvertFrom-StringData @' Initialize_SqlDscRS_ShouldProcessCaption = Initialize Reporting Services instance Initialize_SqlDscRS_FailedToInitialize = Failed to initialize Reporting Services instance '{0}'. {1} (ISRS0001) + ## Get-SqlDscRSIPAddress + Get_SqlDscRSIPAddress_Getting = Getting available IP addresses for Reporting Services instance '{0}'. + Get_SqlDscRSIPAddress_FailedToGet = Failed to get available IP addresses for Reporting Services instance '{0}'. {1} (GSRSIP0001) + + ## Get-SqlDscRSDatabaseInstallation + Get_SqlDscRSDatabaseInstallation_Getting = Getting report server installations registered in the database for Reporting Services instance '{0}'. + Get_SqlDscRSDatabaseInstallation_FailedToGet = Failed to get report server installations for Reporting Services instance '{0}'. {1} (GSRSDI0001) + ## Request-SqlDscRSDatabaseUpgradeScript Request_SqlDscRSDatabaseUpgradeScript_Generating = Generating database upgrade script for Reporting Services instance '{0}'. Request_SqlDscRSDatabaseUpgradeScript_FailedToGenerate = Failed to generate database upgrade script for Reporting Services instance '{0}'. {1} (RSRSDBUS0001) diff --git a/tests/Integration/Commands/Get-SqlDscRSDatabaseInstallation.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSDatabaseInstallation.Integration.Tests.ps1 new file mode 100644 index 000000000..c461c404c --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSDatabaseInstallation.Integration.Tests.ps1 @@ -0,0 +1,138 @@ +[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-SqlDscRSDatabaseInstallation' { + Context 'When getting database installation status for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:expectedMachineName = Get-ComputerName + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + # Strip braces as Get-SqlDscRSDatabaseInstallation returns IDs without braces + $script:expectedInstallationID = $script:configuration.InstallationID -replace '[{}]' + } + + It 'Should return database installation information' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 1 + $result[0].MachineName | Should -Be $script:expectedMachineName + $result[0].InstanceName | Should -Be 'SSRS' + $result[0].IsInitialized | Should -BeTrue + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + + It 'Should return the same installation ID on subsequent calls' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + } + + Context 'When getting database installation status for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:expectedMachineName = Get-ComputerName + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + # Strip braces as Get-SqlDscRSDatabaseInstallation returns IDs without braces + $script:expectedInstallationID = $script:configuration.InstallationID -replace '[{}]' + } + + It 'Should return database installation information' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 1 + $result[0].MachineName | Should -Be $script:expectedMachineName + $result[0].InstanceName | Should -Be 'SSRS' + $result[0].IsInitialized | Should -BeTrue + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + + It 'Should return the same installation ID on subsequent calls' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + } + + Context 'When getting database installation status for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:expectedMachineName = Get-ComputerName + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + # Strip braces as Get-SqlDscRSDatabaseInstallation returns IDs without braces + $script:expectedInstallationID = $script:configuration.InstallationID -replace '[{}]' + } + + It 'Should return database installation information' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 1 + $result[0].MachineName | Should -Be $script:expectedMachineName + $result[0].InstanceName | Should -Be 'SSRS' + $result[0].IsInitialized | Should -BeTrue + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + + It 'Should return the same installation ID on subsequent calls' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + } + + Context 'When getting database installation status for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:expectedMachineName = Get-ComputerName + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + # Strip braces as Get-SqlDscRSDatabaseInstallation returns IDs without braces + $script:expectedInstallationID = $script:configuration.InstallationID -replace '[{}]' + } + + It 'Should return database installation information' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 1 + $result[0].MachineName | Should -Be $script:expectedMachineName + $result[0].InstanceName | Should -Be 'PBIRS' + $result[0].IsInitialized | Should -BeTrue + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + + It 'Should return the same installation ID on subsequent calls' { + $result = @($script:configuration | Get-SqlDscRSDatabaseInstallation -ErrorAction 'Stop') + + $result[0].InstallationID | Should -Be $script:expectedInstallationID + } + } +} diff --git a/tests/Integration/Commands/Get-SqlDscRSExecutionLog.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSExecutionLog.Integration.Tests.ps1 new file mode 100644 index 000000000..3e6e905bb --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSExecutionLog.Integration.Tests.ps1 @@ -0,0 +1,113 @@ +[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-SqlDscRSExecutionLog' { + Context 'When querying execution log for SQL Server Reporting Services instance' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { + It 'Should query the execution log without errors' { + # Query may return empty results if no reports have been executed + $result = Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -MaxRows 10 -Force -ErrorAction 'Stop' + + Write-Verbose -Message ('Result object: {0}' -f ($result | Out-String)) -Verbose + + # Result may be null if no executions exist, but should not throw + # If there are results, verify they have expected properties + if ($result) + { + $result[0].PSObject.Properties.Name | Should -Contain 'ItemPath' + $result[0].PSObject.Properties.Name | Should -Contain 'UserName' + $result[0].PSObject.Properties.Name | Should -Contain 'TimeStart' + $result[0].PSObject.Properties.Name | Should -Contain 'Status' + } + } + + It 'Should be able to filter by MaxRows' { + $result = Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -MaxRows 5 -Force -ErrorAction 'Stop' + + Write-Verbose -Message ('Result object: {0}' -f ($result | Out-String)) -Verbose + + if ($result) + { + @($result).Count | Should -BeLessOrEqual 5 + } + } + + It 'Should be able to filter by date range' { + $startTime = (Get-Date).AddDays(-30) + $endTime = Get-Date + + # Should not throw with date filters + $result = Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -StartTime $startTime -EndTime $endTime -MaxRows 10 -Force -ErrorAction 'Stop' + + Write-Verbose -Message ('Result object: {0}' -f ($result | Out-String)) -Verbose + + # Verify all returned entries are within the date range if any exist + if ($result) + { + foreach ($entry in $result) + { + $entry.TimeStart | Should -BeGreaterOrEqual $startTime + $entry.TimeStart | Should -BeLessOrEqual $endTime + } + } + } + } + + Context 'When querying execution log for Power BI Report Server instance' -Tag @('Integration_PowerBI') { + # cSpell: ignore PBIRS + It 'Should query the execution log without errors' { + # Query may return empty results if no reports have been executed + $result = Get-SqlDscRSExecutionLog -InstanceName 'PBIRS' -MaxRows 10 -Force -ErrorAction 'Stop' + + Write-Verbose -Message ('Result object: {0}' -f ($result | Out-String)) -Verbose + + # Result may be null if no executions exist, but should not throw + # If there are results, verify they have expected properties + if ($result) + { + $result[0].PSObject.Properties.Name | Should -Contain 'ItemPath' + $result[0].PSObject.Properties.Name | Should -Contain 'UserName' + $result[0].PSObject.Properties.Name | Should -Contain 'TimeStart' + $result[0].PSObject.Properties.Name | Should -Contain 'Status' + } + } + + It 'Should be able to filter by MaxRows' { + $result = Get-SqlDscRSExecutionLog -InstanceName 'PBIRS' -MaxRows 5 -Force -ErrorAction 'Stop' + + Write-Verbose -Message ('Result object: {0}' -f ($result | Out-String)) -Verbose + + if ($result) + { + @($result).Count | Should -BeLessOrEqual 5 + } + } + } +} diff --git a/tests/Integration/Commands/Get-SqlDscRSIPAddress.Integration.Tests.ps1 b/tests/Integration/Commands/Get-SqlDscRSIPAddress.Integration.Tests.ps1 new file mode 100644 index 000000000..11702ea0f --- /dev/null +++ b/tests/Integration/Commands/Get-SqlDscRSIPAddress.Integration.Tests.ps1 @@ -0,0 +1,112 @@ +[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-SqlDscRSIPAddress' { + <# + Note: CI environments contain dynamic IP addresses that change per run (e.g., 10.1.0.x, 172.x.x.x). + We validate the command returns expected addresses and structure without checking for specific + dynamic IPs. Loopback addresses (127.0.0.1) and IPv4/IPv6 support are consistent across runs. + #> + + Context 'When getting IP addresses for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + It 'Should return available IP addresses' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSIPAddress -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + + # Should contain loopback addresses (stable across CI workers) + $result.IPAddress | Should -Contain '127.0.0.1' + + # Should have IPv4 addresses + $result.IPVersion | Should -Contain 'V4' + + # Should have at least one additional IP besides loopback + $result.Count | Should -BeGreaterThan 1 + } + } + + Context 'When getting IP addresses for SQL Server Reporting Services' -Tag @('Integration_SQL2019_RS') { + It 'Should return available IP addresses' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSIPAddress -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + + # Should contain loopback addresses (stable across CI workers) + $result.IPAddress | Should -Contain '127.0.0.1' + + # Should have IPv4 addresses + $result.IPVersion | Should -Contain 'V4' + + # Should have at least one additional IP besides loopback + $result.Count | Should -BeGreaterThan 1 + } + } + + Context 'When getting IP addresses for SQL Server Reporting Services' -Tag @('Integration_SQL2022_RS') { + It 'Should return available IP addresses' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSIPAddress -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + + # Should contain loopback addresses (stable across CI workers) + $result.IPAddress | Should -Contain '127.0.0.1' + + # Should have IPv4 addresses + $result.IPVersion | Should -Contain 'V4' + + # Should have at least one additional IP besides loopback + $result.Count | Should -BeGreaterThan 1 + } + } + + Context 'When getting IP addresses for Power BI Report Server' -Tag @('Integration_PowerBI') { + It 'Should return available IP addresses' { + $configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + $result = $configuration | Get-SqlDscRSIPAddress -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + + # Should contain loopback addresses (stable across CI workers) + $result.IPAddress | Should -Contain '127.0.0.1' + + # Should have IPv4 addresses + $result.IPVersion | Should -Contain 'V4' + + # Should have at least one additional IP besides loopback + $result.Count | Should -BeGreaterThan 1 + } + } +} diff --git a/tests/Unit/Public/Disable-SqlDscRsSecureConnection.Tests.ps1 b/tests/Unit/Public/Disable-SqlDscRsSecureConnection.Tests.ps1 index 0fac459e8..f76e6db64 100644 --- a/tests/Unit/Public/Disable-SqlDscRsSecureConnection.Tests.ps1 +++ b/tests/Unit/Public/Disable-SqlDscRsSecureConnection.Tests.ps1 @@ -44,41 +44,6 @@ AfterAll { } Describe 'Disable-SqlDscRsSecureConnection' { - BeforeAll { - InModuleScope -ScriptBlock { - function script:Invoke-CimMethod - { - param - ( - [Parameter(ValueFromPipeline = $true)] - [System.Object] - $InputObject, - - [System.String] - $MethodName, - - [System.Collections.Hashtable] - $Arguments - ) - - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - 'StubNotImplemented', - 'StubCalledError', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $MyInvocation.MyCommand - ) - ) - } - } - } - - AfterAll { - InModuleScope -ScriptBlock { - Remove-Item -Path 'function:script:Invoke-CimMethod' -Force - } - } - Context 'When validating parameter sets' { It 'Should have the correct parameters in parameter set ' -ForEach @( @{ @@ -105,7 +70,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { SecureConnectionLevel = 2 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -115,7 +80,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { It 'Should disable secure connection without errors' { { $mockCimInstance | Disable-SqlDscRsSecureConnection -Confirm:$false } | Should -Not -Throw - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'SetSecureConnectionLevel' -and $Arguments.Level -eq 0 } -Exactly -Times 1 @@ -135,7 +100,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { SecureConnectionLevel = 2 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -157,7 +122,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { SecureConnectionLevel = 2 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -167,49 +132,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { It 'Should disable secure connection without confirmation' { { $mockCimInstance | Disable-SqlDscRsSecureConnection -Force } | Should -Not -Throw - Should -Invoke -CommandName Invoke-CimMethod -Exactly -Times 1 - } - } - - Context 'When CIM method fails with ExtendedErrors' { - BeforeAll { - $mockCimInstance = [PSCustomObject] @{ - InstanceName = 'SSRS' - SecureConnectionLevel = 2 - } - - Mock -CommandName Invoke-CimMethod -MockWith { - $result = [PSCustomObject] @{ - HRESULT = -2147024891 - ExtendedErrors = @('Access denied', 'Permission error') - } - $result | Add-Member -MemberType ScriptMethod -Name 'GetType' -Value { return [PSCustomObject] @{ Name = 'CimMethodResult' } } -Force - return $result - } - } - - It 'Should throw a terminating error' { - { $mockCimInstance | Disable-SqlDscRsSecureConnection -Confirm:$false } | Should -Throw -ErrorId 'DSRSSC0001,Disable-SqlDscRsSecureConnection' - } - } - - Context 'When CIM method fails with Error property' { - BeforeAll { - $mockCimInstance = [PSCustomObject] @{ - InstanceName = 'SSRS' - SecureConnectionLevel = 2 - } - - Mock -CommandName Invoke-CimMethod -MockWith { - return [PSCustomObject] @{ - HRESULT = -2147024891 - Error = 'Access denied' - } - } - } - - It 'Should throw a terminating error' { - { $mockCimInstance | Disable-SqlDscRsSecureConnection -Confirm:$false } | Should -Throw -ErrorId 'DSRSSC0001,Disable-SqlDscRsSecureConnection' + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 } } @@ -220,13 +143,13 @@ Describe 'Disable-SqlDscRsSecureConnection' { SecureConnectionLevel = 2 } - Mock -CommandName Invoke-CimMethod + Mock -CommandName Invoke-RsCimMethod } - It 'Should not call Invoke-CimMethod' { + It 'Should not call Invoke-RsCimMethod' { $mockCimInstance | Disable-SqlDscRsSecureConnection -WhatIf - Should -Invoke -CommandName Invoke-CimMethod -Exactly -Times 0 + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 } } @@ -237,7 +160,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { SecureConnectionLevel = 2 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -247,7 +170,7 @@ Describe 'Disable-SqlDscRsSecureConnection' { It 'Should disable secure connection' { { Disable-SqlDscRsSecureConnection -Configuration $mockCimInstance -Confirm:$false } | Should -Not -Throw - Should -Invoke -CommandName Invoke-CimMethod -Exactly -Times 1 + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 } } } diff --git a/tests/Unit/Public/Enable-SqlDscRsSecureConnection.Tests.ps1 b/tests/Unit/Public/Enable-SqlDscRsSecureConnection.Tests.ps1 index 6c113bb5e..3daf69bb7 100644 --- a/tests/Unit/Public/Enable-SqlDscRsSecureConnection.Tests.ps1 +++ b/tests/Unit/Public/Enable-SqlDscRsSecureConnection.Tests.ps1 @@ -44,41 +44,6 @@ AfterAll { } Describe 'Enable-SqlDscRsSecureConnection' { - BeforeAll { - InModuleScope -ScriptBlock { - function script:Invoke-CimMethod - { - param - ( - [Parameter(ValueFromPipeline = $true)] - [System.Object] - $InputObject, - - [System.String] - $MethodName, - - [System.Collections.Hashtable] - $Arguments - ) - - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - 'StubNotImplemented', - 'StubCalledError', - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $MyInvocation.MyCommand - ) - ) - } - } - } - - AfterAll { - InModuleScope -ScriptBlock { - Remove-Item -Path 'function:script:Invoke-CimMethod' -Force - } - } - Context 'When validating parameter sets' { It 'Should have the correct parameters in parameter set ' -ForEach @( @{ @@ -105,7 +70,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { SecureConnectionLevel = 0 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -115,7 +80,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { It 'Should enable secure connection without errors' { { $mockCimInstance | Enable-SqlDscRsSecureConnection -Confirm:$false } | Should -Not -Throw - Should -Invoke -CommandName Invoke-CimMethod -ParameterFilter { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'SetSecureConnectionLevel' -and $Arguments.Level -eq 1 } -Exactly -Times 1 @@ -135,7 +100,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { SecureConnectionLevel = 0 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -157,7 +122,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { SecureConnectionLevel = 0 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -167,49 +132,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { It 'Should enable secure connection without confirmation' { { $mockCimInstance | Enable-SqlDscRsSecureConnection -Force } | Should -Not -Throw - Should -Invoke -CommandName Invoke-CimMethod -Exactly -Times 1 - } - } - - Context 'When CIM method fails with ExtendedErrors' { - BeforeAll { - $mockCimInstance = [PSCustomObject] @{ - InstanceName = 'SSRS' - SecureConnectionLevel = 0 - } - - Mock -CommandName Invoke-CimMethod -MockWith { - $result = [PSCustomObject] @{ - HRESULT = -2147024891 - ExtendedErrors = @('Access denied', 'Permission error') - } - $result | Add-Member -MemberType ScriptMethod -Name 'GetType' -Value { return [PSCustomObject] @{ Name = 'CimMethodResult' } } -Force - return $result - } - } - - It 'Should throw a terminating error' { - { $mockCimInstance | Enable-SqlDscRsSecureConnection -Confirm:$false } | Should -Throw -ErrorId 'ESRSSC0001,Enable-SqlDscRsSecureConnection' - } - } - - Context 'When CIM method fails with Error property' { - BeforeAll { - $mockCimInstance = [PSCustomObject] @{ - InstanceName = 'SSRS' - SecureConnectionLevel = 0 - } - - Mock -CommandName Invoke-CimMethod -MockWith { - return [PSCustomObject] @{ - HRESULT = -2147024891 - Error = 'Access denied' - } - } - } - - It 'Should throw a terminating error' { - { $mockCimInstance | Enable-SqlDscRsSecureConnection -Confirm:$false } | Should -Throw -ErrorId 'ESRSSC0001,Enable-SqlDscRsSecureConnection' + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 } } @@ -220,13 +143,13 @@ Describe 'Enable-SqlDscRsSecureConnection' { SecureConnectionLevel = 0 } - Mock -CommandName Invoke-CimMethod + Mock -CommandName Invoke-RsCimMethod } - It 'Should not call Invoke-CimMethod' { + It 'Should not call Invoke-RsCimMethod' { $mockCimInstance | Enable-SqlDscRsSecureConnection -WhatIf - Should -Invoke -CommandName Invoke-CimMethod -Exactly -Times 0 + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 } } @@ -237,7 +160,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { SecureConnectionLevel = 0 } - Mock -CommandName Invoke-CimMethod -MockWith { + Mock -CommandName Invoke-RsCimMethod -MockWith { return [PSCustomObject] @{ HRESULT = 0 } @@ -247,7 +170,7 @@ Describe 'Enable-SqlDscRsSecureConnection' { It 'Should enable secure connection' { { Enable-SqlDscRsSecureConnection -Configuration $mockCimInstance -Confirm:$false } | Should -Not -Throw - Should -Invoke -CommandName Invoke-CimMethod -Exactly -Times 1 + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 } } } diff --git a/tests/Unit/Public/Get-SqlDscRSDatabaseInstallation.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSDatabaseInstallation.Tests.ps1 new file mode 100644 index 000000000..51fa14f84 --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSDatabaseInstallation.Tests.ps1 @@ -0,0 +1,171 @@ +[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-SqlDscRSDatabaseInstallation' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSDatabaseInstallation').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 installations' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + # The CIM method returns GUIDs without braces + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + Length = 2 + InstallationIDs = @('c59fadae-f8ee-46ee-b063-f8d89872100c', 'a1b2c3d4-e5f6-7890-abcd-ef1234567890') + MachineNames = @('SERVER1', 'SERVER2') + InstanceNames = @('SSRS', 'SSRS') + IsInitialized = @($true, $true) + } + } + } + + It 'Should return installation objects with GUIDs without braces' { + $result = $mockCimInstance | Get-SqlDscRSDatabaseInstallation + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 2 + $result[0].InstallationID | Should -Be 'c59fadae-f8ee-46ee-b063-f8d89872100c' + $result[0].MachineName | Should -Be 'SERVER1' + $result[0].InstanceName | Should -Be 'SSRS' + $result[0].IsInitialized | Should -BeTrue + $result[1].InstallationID | Should -Be 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ListReportServersInDatabase' + } -Exactly -Times 1 + } + } + + Context 'When there are no installations' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + Length = 0 + InstallationIDs = @() + MachineNames = @() + InstanceNames = @() + IsInitialized = @() + } + } + } + + It 'Should return an empty result' { + $result = $mockCimInstance | Get-SqlDscRSDatabaseInstallation + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method ListReportServersInDatabase() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Get-SqlDscRSDatabaseInstallation } | Should -Throw -ErrorId 'GSRSDI0001,Get-SqlDscRSDatabaseInstallation' + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + # The CIM method returns GUIDs without braces + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + Length = 1 + InstallationIDs = @('d4e5f6a7-b8c9-0123-4567-890abcdef012') + MachineNames = @('SERVER1') + InstanceNames = @('SSRS') + IsInitialized = @($true) + } + } + } + + It 'Should get database installation information' { + $result = Get-SqlDscRSDatabaseInstallation -Configuration $mockCimInstance + + $result | Should -Not -BeNullOrEmpty + $result | Should -HaveCount 1 + $result.InstallationID | Should -Be 'd4e5f6a7-b8c9-0123-4567-890abcdef012' + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSExecutionLog.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSExecutionLog.Tests.ps1 new file mode 100644 index 000000000..1f9fa895e --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSExecutionLog.Tests.ps1 @@ -0,0 +1,372 @@ +[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' + + # Loading mocked classes + Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs') + + $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-SqlDscRSExecutionLog' { + Context 'When parameter validation' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-InstanceName] [[-StartTime] ] [[-EndTime] ] [[-UserName] ] [[-ReportPath] ] [[-MaxRows] ] [[-Credential] ] [[-LoginType] ] [[-StatementTimeout] ] [-Encrypt] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSExecutionLog').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + + It 'Should have InstanceName as a mandatory parameter' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSExecutionLog').Parameters['InstanceName'] + + $parameterInfo.Attributes.Mandatory | Should -BeTrue + } + + It 'Should have MaxRows with a default value of 1000' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSExecutionLog').Parameters['MaxRows'] + + # Find the default value by examining parameter attributes or testing the command + $parameterInfo | Should -Not -BeNullOrEmpty + } + + It 'Should have LoginType with a default value of Integrated' { + $parameterInfo = (Get-Command -Name 'Get-SqlDscRSExecutionLog').Parameters['LoginType'] + + $parameterInfo | Should -Not -BeNullOrEmpty + } + } + + Context 'When querying the execution log successfully' { + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + DatabaseServerName = 'localhost' + DatabaseName = 'ReportServer' + } + } + + # Create a mock DataSet with results + $mockDataTable = [System.Data.DataTable]::new() + $null = $mockDataTable.Columns.Add('InstanceName', [System.String]) + $null = $mockDataTable.Columns.Add('ItemPath', [System.String]) + $null = $mockDataTable.Columns.Add('UserName', [System.String]) + $null = $mockDataTable.Columns.Add('TimeStart', [System.DateTime]) + $null = $mockDataTable.Columns.Add('Status', [System.String]) + + $mockRow = $mockDataTable.NewRow() + $mockRow['InstanceName'] = 'SSRS' + $mockRow['ItemPath'] = '/Sales/Revenue' + $mockRow['UserName'] = 'DOMAIN\TestUser' + $mockRow['TimeStart'] = [System.DateTime]::new(2025, 1, 1, 10, 0, 0) + $mockRow['Status'] = 'rsSuccess' + $mockDataTable.Rows.Add($mockRow) + + $mockDataSet = [System.Data.DataSet]::new() + $mockDataSet.Tables.Add($mockDataTable) + + Mock -CommandName Invoke-SqlDscQuery -MockWith { + return $mockDataSet + } + } + + It 'Should return execution log entries' { + $result = Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Force + + $result | Should -Not -BeNullOrEmpty + $result.ItemPath | Should -Be '/Sales/Revenue' + $result.UserName | Should -Be 'DOMAIN\TestUser' + + Should -Invoke -CommandName Get-SqlDscRSConfiguration -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-SqlDscQuery -Exactly -Times 1 -Scope It + } + + It 'Should pass the correct parameters to Invoke-SqlDscQuery' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $ServerName -eq 'localhost' -and + $InstanceName -eq 'MSSQLSERVER' -and + $DatabaseName -eq 'ReportServer' -and + $PassThru -eq $true -and + $Force -eq $true + } -Exactly -Times 1 -Scope It + } + } + + Context 'When the database server name contains a named instance' { + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + DatabaseServerName = 'SqlServer01\RSDB' + DatabaseName = 'ReportServer' + } + } + + $mockDataSet = [System.Data.DataSet]::new() + $mockDataTable = [System.Data.DataTable]::new() + $mockDataSet.Tables.Add($mockDataTable) + + Mock -CommandName Invoke-SqlDscQuery -MockWith { + return $mockDataSet + } + } + + It 'Should parse the server and instance name correctly' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $ServerName -eq 'SqlServer01' -and + $InstanceName -eq 'RSDB' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When using filter parameters' { + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + DatabaseServerName = 'localhost' + DatabaseName = 'ReportServer' + } + } + + $mockDataSet = [System.Data.DataSet]::new() + $mockDataTable = [System.Data.DataTable]::new() + $mockDataSet.Tables.Add($mockDataTable) + + Mock -CommandName Invoke-SqlDscQuery -MockWith { + return $mockDataSet + } + } + + It 'Should include StartTime filter in the query' { + $startTime = [System.DateTime]::new(2025, 1, 1) + + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -StartTime $startTime -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Query -match "TimeStart >= '2025-01-01 00:00:00'" + } -Exactly -Times 1 -Scope It + } + + It 'Should include EndTime filter in the query' { + $endTime = [System.DateTime]::new(2025, 12, 31, 23, 59, 59) + + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -EndTime $endTime -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Query -match "TimeStart <= '2025-12-31 23:59:59'" + } -Exactly -Times 1 -Scope It + } + + It 'Should include UserName filter in the query' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -UserName 'DOMAIN\%' -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Query -match "UserName LIKE 'DOMAIN\\%'" + } -Exactly -Times 1 -Scope It + } + + It 'Should include ReportPath filter in the query' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -ReportPath '/Sales/%' -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Query -match "ItemPath LIKE '/Sales/%'" + } -Exactly -Times 1 -Scope It + } + + It 'Should include MaxRows in the query' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -MaxRows 500 -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Query -match 'TOP \(500\)' + } -Exactly -Times 1 -Scope It + } + + It 'Should not include TOP clause when MaxRows is 0' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -MaxRows 0 -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Query -notmatch 'TOP' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When using credential parameters' { + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + DatabaseServerName = 'localhost' + DatabaseName = 'ReportServer' + } + } + + $mockDataSet = [System.Data.DataSet]::new() + $mockDataTable = [System.Data.DataTable]::new() + $mockDataSet.Tables.Add($mockDataTable) + + Mock -CommandName Invoke-SqlDscQuery -MockWith { + return $mockDataSet + } + } + + It 'Should pass Credential to Invoke-SqlDscQuery' { + $securePassword = ConvertTo-SecureString -String 'TestPassword' -AsPlainText -Force + $credential = [System.Management.Automation.PSCredential]::new('TestUser', $securePassword) + + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Credential $credential -LoginType 'SqlLogin' -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $null -ne $Credential -and + $LoginType -eq 'SqlLogin' + } -Exactly -Times 1 -Scope It + } + + It 'Should pass Encrypt to Invoke-SqlDscQuery' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Encrypt -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $Encrypt -eq $true + } -Exactly -Times 1 -Scope It + } + + It 'Should pass StatementTimeout to Invoke-SqlDscQuery' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -StatementTimeout 120 -Force + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $StatementTimeout -eq 120 + } -Exactly -Times 1 -Scope It + } + } + + Context 'When the query fails' { + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + DatabaseServerName = 'localhost' + DatabaseName = 'ReportServer' + } + } + + Mock -CommandName Invoke-SqlDscQuery -MockWith { + throw 'Connection failed' + } + } + + It 'Should write an error and return null' { + $result = Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -Force -ErrorAction SilentlyContinue -ErrorVariable testError 3>&1 4>&1 5>&1 6>&1 + + $result | Should -BeNullOrEmpty + $testError | Should -Not -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-SqlDscQuery -Exactly -Times 1 -Scope It + } + } + + Context 'When querying Power BI Report Server' { + # cSpell: ignore PBIRS + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'PBIRS' + DatabaseServerName = 'localhost' + DatabaseName = 'ReportServerPBIRS' + } + } + + $mockDataSet = [System.Data.DataSet]::new() + $mockDataTable = [System.Data.DataTable]::new() + $mockDataSet.Tables.Add($mockDataTable) + + Mock -CommandName Invoke-SqlDscQuery -MockWith { + return $mockDataSet + } + } + + It 'Should query the correct database for PBIRS' { + Get-SqlDscRSExecutionLog -InstanceName 'PBIRS' -Force + + Should -Invoke -CommandName Get-SqlDscRSConfiguration -ParameterFilter { + $InstanceName -eq 'PBIRS' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-SqlDscQuery -ParameterFilter { + $DatabaseName -eq 'ReportServerPBIRS' + } -Exactly -Times 1 -Scope It + } + } + + Context 'When WhatIf is used' { + BeforeAll { + Mock -CommandName Get-SqlDscRSConfiguration -MockWith { + return [PSCustomObject]@{ + InstanceName = 'SSRS' + DatabaseServerName = 'localhost' + DatabaseName = 'ReportServer' + } + } + + Mock -CommandName Invoke-SqlDscQuery + } + + It 'Should not execute the query' { + Get-SqlDscRSExecutionLog -InstanceName 'SSRS' -WhatIf + + Should -Invoke -CommandName Invoke-SqlDscQuery -Exactly -Times 0 -Scope It + } + } +} diff --git a/tests/Unit/Public/Get-SqlDscRSIPAddress.Tests.ps1 b/tests/Unit/Public/Get-SqlDscRSIPAddress.Tests.ps1 new file mode 100644 index 000000000..8845328ee --- /dev/null +++ b/tests/Unit/Public/Get-SqlDscRSIPAddress.Tests.ps1 @@ -0,0 +1,167 @@ +[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-SqlDscRSIPAddress' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] []' + } + ) { + $result = (Get-Command -Name 'Get-SqlDscRSIPAddress').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 IP addresses successfully' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + IPAddress = @('0.0.0.0', '192.168.1.1', '::') + IPVersion = @('V4', 'V4', 'V6') + IsDhcpEnabled = @($false, $true, $false) + } + } + } + + It 'Should return IP addresses as ReportServerIPAddress objects' { + $result = $mockCimInstance | Get-SqlDscRSIPAddress + + $result | Should -HaveCount 3 + $result[0].IPAddress | Should -Be '0.0.0.0' + $result[0].IPVersion | Should -Be 'V4' + $result[0].IsDhcpEnabled | Should -BeFalse + $result[1].IPAddress | Should -Be '192.168.1.1' + $result[1].IPVersion | Should -Be 'V4' + $result[1].IsDhcpEnabled | Should -BeTrue + $result[2].IPAddress | Should -Be '::' + $result[2].IPVersion | Should -Be 'V6' + $result[2].IsDhcpEnabled | Should -BeFalse + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ListIPAddresses' + } -Exactly -Times 1 + } + } + + Context 'When there are no IP addresses' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + IPAddress = @() + IPVersion = @() + IsDhcpEnabled = @() + } + } + } + + It 'Should return an empty result' { + $result = $mockCimInstance | Get-SqlDscRSIPAddress + + $result | Should -BeNullOrEmpty + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method ListIPAddresses() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Get-SqlDscRSIPAddress } | Should -Throw -ErrorId 'GSRSIP0001,Get-SqlDscRSIPAddress' + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return @{ + IPAddress = @('0.0.0.0') + IPVersion = @('V4') + IsDhcpEnabled = @($false) + } + } + } + + It 'Should get IP addresses' { + $result = Get-SqlDscRSIPAddress -Configuration $mockCimInstance + + $result | Should -HaveCount 1 + $result[0].IPAddress | Should -Be '0.0.0.0' + $result[0].IPVersion | Should -Be 'V4' + $result[0].IsDhcpEnabled | Should -BeFalse + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } +}