diff --git a/.vscode/settings.json b/.vscode/settings.json index 68a53391b6..0aa4c3b660 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -107,7 +107,8 @@ "varchar", "maxdop", "hotfixes", - "checkpointing" + "checkpointing", + "HRESULT" ], "cSpell.ignorePaths": [ ".git" diff --git a/CHANGELOG.md b/CHANGELOG.md index 000b17b682..8f4b366a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Added public command `Request-SqlDscRSDatabaseScript` to generate T-SQL scripts + for creating report server databases. Wraps the `GenerateDatabaseCreationScript` + CIM method and supports configuring database name, language (LCID), and + SharePoint mode ([issue #2017](https://github.com/dsccommunity/SqlServerDsc/issues/2017)). +- Added public command `Request-SqlDscRSDatabaseRightsScript` to generate T-SQL + scripts for granting permissions on report server databases. Wraps the + `GenerateDatabaseRightsScript` CIM method and supports configuring database + name, user name, remote connections, and Windows/SQL authentication types + ([issue #2019](https://github.com/dsccommunity/SqlServerDsc/issues/2019)). +- Added public command `Set-SqlDscRSDatabaseConnection` to set + the report server database connection for SQL Server Reporting Services or + Power BI Report Server. Wraps the `SetDatabaseConnection` CIM method and + supports Windows, SQL Server, and Service Account authentication types + ([issue #2021](https://github.com/dsccommunity/SqlServerDsc/issues/2021)). - Added public command `Set-SqlDscRSVirtualDirectory` to set the virtual directory for Reporting Services applications. Wraps the `SetVirtualDirectory` CIM method and supports ReportServerWebService, ReportServerWebApp, and ReportManager @@ -47,6 +61,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Services configuration instances with consistent error handling. This function is used by `Enable-SqlDscRsSecureConnection`, `Disable-SqlDscRsSecureConnection`, and the `SqlRS` resource. +- Added private function `Get-HResultMessage` to translate common Windows HRESULT + error codes into human-readable messages. Used by `Invoke-RsCimMethod` to + provide actionable error messages when Reporting Services CIM methods fail + without detailed error information. - `Invoke-ReportServerSetupAction` - Now uses `Format-Path` with `-ExpandEnvironmentVariable` to expand environment variables in all path parameters (`MediaPath`, `LogPath`, `InstallFolder`) @@ -185,6 +203,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Refactored to use the public command `Set-SqlDscRSVirtualDirectory` for setting virtual directories instead of calling the CIM method directly ([issue #2015](https://github.com/dsccommunity/SqlServerDsc/issues/2015)). + - Refactored to use the public commands `Request-SqlDscRSDatabaseScript`, + `Request-SqlDscRSDatabaseRightsScript`, and `Set-SqlDscRSDatabaseConnection` + for creating and configuring the report server database instead of calling + the CIM methods directly + ([issue #2017](https://github.com/dsccommunity/SqlServerDsc/issues/2017)) + ([issue #2019](https://github.com/dsccommunity/SqlServerDsc/issues/2019)) + ([issue #2021](https://github.com/dsccommunity/SqlServerDsc/issues/2021)). - `Assert-SetupActionProperties` - Refactored to use the command `Get-FileVersion` from the DscResource.Common module instead of the private function `Get-FileVersionInformation` @@ -201,6 +226,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `Invoke-RsCimMethod` + - Enhanced error messages to include human-readable translations of common + HRESULT error codes. When Reporting Services CIM methods fail without + detailed error information, the error message now includes actionable + guidance based on the HRESULT code (e.g., service not running, access + denied, logon type not granted). + - Fixed error handling to properly surface error details. Previously, when + the `ExtendedErrors` property existed but was empty, the error message + would show an empty error description. Now it correctly falls back to + the `Error` property and provides a descriptive fallback message if + neither property contains error details. +- Prerequisites Integration Tests + - Fixed missing RS (Reporting Services) integration test tags on Context blocks + that create local Windows users, service accounts, and groups. Added tags + `Integration_SQL2017_RS`, `Integration_SQL2019_RS`, and `Integration_SQL2022_RS` + to ensure these prerequisites run before Reporting Services integration tests. +- `New-SqlDscFileGroup` + - Fixed comment-based help example formatting by moving inline comment + to the description text. +- QA Tests + - Added new test to detect comments within multi-line example code blocks + in comment-based help. Comments in the code portion of `.EXAMPLE` blocks + cause PlatyPS documentation generation to fail with "Expect Heading" errors. + - Added new test to detect blank lines within multi-line example code blocks + in comment-based help. Blank lines within the code portion of `.EXAMPLE` + blocks cause similar issues with documentation generation. +- `Deny-SqlDscServerPermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks that would cause documentation generation issues. +- `Get-SqlDscServerPermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks that would cause documentation generation issues. +- `Grant-SqlDscServerPermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks that would cause documentation generation issues. +- `New-SqlDscDatabase` + - Fixed comment-based help example formatting by removing blank lines + within code blocks. +- `New-SqlDscDatabaseSnapshot` + - Fixed comment-based help example formatting by removing blank lines + within code blocks. +- `Revoke-SqlDscServerPermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks. +- `Set-SqlDscDatabasePermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks. +- `Set-SqlDscServerPermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks. +- `Test-SqlDscServerPermission` + - Fixed comment-based help example formatting by removing blank lines + within code blocks. - Unit Tests - Fixed PowerShell class type identity issues that caused "Cannot convert 'Type' to 'Type'" errors when running multiple test files in the same @@ -260,9 +338,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Set-SqlDscDatabaseProperty` - Updated comment-based help to reference correct enum values. - Added SQL Server version requirements to version-specific parameter help. - -### Fixed - - `DatabasePermission` - Fixed `Equals()` method to compare both `State` and `Permission` properties. Previously, the method incorrectly referenced a non-existent `Grant` property, diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 260faef270..a0dd77bba4 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -530,6 +530,7 @@ stages: # Run the integration tests in a specific group order. # Group 0 'tests/Integration/Commands/Prerequisites.Integration.Tests.ps1' + 'tests/Integration/Commands/Prerequisites.RSDB.Integration.Tests.ps1' 'tests/Integration/Commands/Save-SqlDscSqlServerMediaFile.Integration.Tests.ps1' 'tests/Integration/Commands/Import-SqlDscPreferredModule.Integration.Tests.ps1' # Group 1 @@ -539,6 +540,8 @@ stages: 'tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSSetupConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInstalled.Integration.Tests.ps1' + 'tests/Integration/Commands/Request-SqlDscRSDatabaseScript.Integration.Tests.ps1' + 'tests/Integration/Commands/Request-SqlDscRSDatabaseRightsScript.Integration.Tests.ps1' # Group 3 'tests/Integration/Commands/Get-SqlDscRSConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Enable-SqlDscRsSecureConnection.Integration.Tests.ps1' @@ -548,6 +551,7 @@ stages: 'tests/Integration/Commands/Add-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSUrlReservation.Integration.Tests.ps1' + 'tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1' # Group 8 'tests/Integration/Commands/Repair-SqlDscReportingService.Integration.Tests.ps1' # Group 9 @@ -605,6 +609,7 @@ stages: # Run the integration tests in a specific group order. # Group 0 'tests/Integration/Commands/Prerequisites.Integration.Tests.ps1' + 'tests/Integration/Commands/Prerequisites.RSDB.Integration.Tests.ps1' 'tests/Integration/Commands/Save-SqlDscSqlServerMediaFile.Integration.Tests.ps1' 'tests/Integration/Commands/Import-SqlDscPreferredModule.Integration.Tests.ps1' # Group 1 @@ -614,6 +619,8 @@ stages: 'tests/Integration/Commands/Get-SqlDscRSPackage.Integration.Tests.ps1' 'tests/Integration/Commands/Get-SqlDscRSSetupConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Test-SqlDscRSInstalled.Integration.Tests.ps1' + 'tests/Integration/Commands/Request-SqlDscRSDatabaseScript.Integration.Tests.ps1' + 'tests/Integration/Commands/Request-SqlDscRSDatabaseRightsScript.Integration.Tests.ps1' # Group 3 'tests/Integration/Commands/Get-SqlDscRSConfiguration.Integration.Tests.ps1' 'tests/Integration/Commands/Enable-SqlDscRsSecureConnection.Integration.Tests.ps1' @@ -623,6 +630,7 @@ stages: 'tests/Integration/Commands/Add-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Remove-SqlDscRSUrlReservation.Integration.Tests.ps1' 'tests/Integration/Commands/Set-SqlDscRSUrlReservation.Integration.Tests.ps1' + 'tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1' # Group 8 'tests/Integration/Commands/Repair-SqlDscPowerBIReportServer.Integration.Tests.ps1' # Group 9 diff --git a/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 b/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 index bbf82aa16f..8b0d8b5a7e 100644 --- a/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 +++ b/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 @@ -396,37 +396,14 @@ function Set-TargetResource Write-Verbose -Message "Generate database creation script on $DatabaseServerName\$DatabaseInstanceName for database '$reportingServicesDatabaseName'." - $invokeRsCimMethodParameters = @{ - CimInstance = $rsConfiguration - MethodName = 'GenerateDatabaseCreationScript' - Arguments = @{ - DatabaseName = $reportingServicesDatabaseName - IsSharePointMode = $false - Lcid = $language - } - } + $reportingServicesDatabaseScript = $rsConfiguration | Request-SqlDscRSDatabaseScript -DatabaseName $reportingServicesDatabaseName -Lcid $language -ErrorAction 'Stop' - $reportingServicesDatabaseScript = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' - - # Determine RS service account - $reportingServicesServiceAccountUserName = (Get-CimInstance -ClassName Win32_Service | Where-Object -FilterScript { - $_.Name -eq $reportingServicesServiceName - }).StartName + # The WindowsServiceIdentityActual property contains the actual account name actively used by the service. + $reportingServicesServiceAccountUserName = $rsConfiguration.WindowsServiceIdentityActual Write-Verbose -Message "Generate database rights script on $DatabaseServerName\$DatabaseInstanceName for database '$reportingServicesDatabaseName' and user '$reportingServicesServiceAccountUserName'." - $invokeRsCimMethodParameters = @{ - CimInstance = $rsConfiguration - MethodName = 'GenerateDatabaseRightsScript' - Arguments = @{ - DatabaseName = $reportingServicesDatabaseName - UserName = $reportingServicesServiceAccountUserName - IsRemote = $false - IsWindowsUser = $true - } - } - - $reportingServicesDatabaseRightsScript = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + $reportingServicesDatabaseRightsScript = $rsConfiguration | Request-SqlDscRSDatabaseRightsScript -DatabaseName $reportingServicesDatabaseName -UserName $reportingServicesServiceAccountUserName -ErrorAction 'Stop' Import-SqlDscPreferredModule @@ -444,49 +421,25 @@ function Set-TargetResource $invokeSqlDscQueryParameters.Encrypt = $true } - Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $reportingServicesDatabaseScript.Script - Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $reportingServicesDatabaseRightsScript.Script + Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $reportingServicesDatabaseScript + Invoke-SqlDscQuery @invokeSqlDscQueryParameters -Query $reportingServicesDatabaseRightsScript Write-Verbose -Message "Set database connection on $DatabaseServerName\$DatabaseInstanceName to database '$reportingServicesDatabaseName'." - if ( $DatabaseInstanceName -eq 'MSSQLSERVER' ) - { - $reportingServicesConnection = $DatabaseServerName + $setSqlDscRSDatabaseConnectionParameters = @{ + ServerName = $DatabaseServerName + DatabaseName = $reportingServicesDatabaseName + Type = 'ServiceAccount' + Force = $true + ErrorAction = 'Stop' } - else + + if ($DatabaseInstanceName -ne 'MSSQLSERVER') { - $reportingServicesConnection = "$DatabaseServerName\$DatabaseInstanceName" - } - - $invokeRsCimMethodParameters = @{ - CimInstance = $rsConfiguration - MethodName = 'SetDatabaseConnection' - Arguments = @{ - Server = $reportingServicesConnection - DatabaseName = $reportingServicesDatabaseName - Username = '' - Password = '' - - <# - Can be set to either: - 0 = Windows - 1 = Sql Server - 2 = Windows Service (Integrated Security) - - When set to 2 the Reporting Server Web service will use - either the ASP.NET account or an application pool’s account - and the Windows service account to access the report server - database. - - See more in the article - https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-setdatabaseconnection#remarks - - #> - CredentialsType = 2 - } + $setSqlDscRSDatabaseConnectionParameters.InstanceName = $DatabaseInstanceName } - Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + $rsConfiguration | Set-SqlDscRSDatabaseConnection @setSqlDscRSDatabaseConnectionParameters <# When initializing SSRS 2019, the call to InitializeReportServer diff --git a/source/Private/Get-HResultMessage.ps1 b/source/Private/Get-HResultMessage.ps1 new file mode 100644 index 0000000000..b122f4f1f3 --- /dev/null +++ b/source/Private/Get-HResultMessage.ps1 @@ -0,0 +1,102 @@ +<# + .SYNOPSIS + Gets a human-readable message for a given HRESULT code. + + .DESCRIPTION + Translates common Windows HRESULT error codes into human-readable + messages. This is particularly useful when CIM methods return an + HRESULT code without detailed error information in ExtendedErrors + or Error properties. + + .PARAMETER HResult + The HRESULT code to translate. This is typically a 32-bit signed + integer returned from a Windows API or CIM method call. + + .OUTPUTS + `System.String` + + Returns a descriptive message for known HRESULT codes, or a generic + message with the hexadecimal code for unknown values. + + .EXAMPLE + Get-HResultMessage -HResult -2147023181 + + Returns: The account has not been granted the requested logon type at + this computer. Verify that the service account has the required + permissions to interact with the Reporting Services WMI provider. + + .EXAMPLE + Get-HResultMessage -HResult -2147024891 + + Returns: Access is denied. Verify that the current user has administrator + rights on the Reporting Services instance. + + .NOTES + This function is used internally by other commands to provide actionable + error messages when Reporting Services CIM methods fail without detailed + error information. These codes have not been verified against any official + Microsoft documentation, and based on the common HRESULT values in + https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values. +#> +function Get-HResultMessage +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.Int32] + $HResult + ) + + <# + HRESULT values are 32-bit signed integers. Negative values indicate + errors. The HRESULT is composed of: + - Bit 31: Severity (0 = success, 1 = error) + - Bits 16-30: Facility code + - Bits 0-15: Error code + + Common HRESULT values are documented at: + https://learn.microsoft.com/en-us/windows/win32/seccrypto/common-hresult-values + #> + $hResultMessages = @{ + # cSpell: ignore ACCESSDENIED LOGON + # E_ACCESSDENIED (0x80070005) - General access denied error + -2147024891 = $script:localizedData.HResult_AccessDenied + + # ERROR_LOGON_TYPE_NOT_GRANTED (0x80070533) - Account lacks logon rights + -2147023181 = $script:localizedData.HResult_LogonTypeNotGranted + + # E_FAIL (0x80004005) - Unspecified failure + -2147467259 = $script:localizedData.HResult_UnspecifiedFailure + + # E_INVALIDARG (0x80070057) - One or more arguments are invalid + -2147024809 = $script:localizedData.HResult_InvalidArgument + + # E_OUTOFMEMORY (0x8007000E) - Out of memory + -2147024882 = $script:localizedData.HResult_OutOfMemory + + # RPC_E_DISCONNECTED (0x80010108) - The object invoked has disconnected + -2147417848 = $script:localizedData.HResult_RpcDisconnected + + # RPC_S_SERVER_UNAVAILABLE (0x800706BA) - The RPC server is unavailable + -2147023174 = $script:localizedData.HResult_RpcServerUnavailable + + # ERROR_SERVICE_NOT_ACTIVE (0x80070426) - The service has not been started + -2147023834 = $script:localizedData.HResult_ServiceNotActive + } + + if ($hResultMessages.ContainsKey($HResult)) + { + return $hResultMessages[$HResult] + } + + <# + Return a generic message with the hexadecimal representation for unknown codes. + Convert to hex using bitwise operation to handle negative values that would + overflow when casting directly to UInt32 (e.g., Int32.MinValue = -2147483648). + #> + $hexValue = '0x{0:X8}' -f ($HResult -band 0xFFFFFFFF) + + return ($script:localizedData.HResult_Unknown -f $hexValue) +} diff --git a/source/Private/Invoke-RsCimMethod.ps1 b/source/Private/Invoke-RsCimMethod.ps1 index 52c303ec96..d220dacefa 100644 --- a/source/Private/Invoke-RsCimMethod.ps1 +++ b/source/Private/Invoke-RsCimMethod.ps1 @@ -75,20 +75,34 @@ function Invoke-RsCimMethod #> if ($invokeCimMethodResult -and $invokeCimMethodResult.HRESULT -ne 0) { - if ($invokeCimMethodResult | Get-Member -Name 'ExtendedErrors') + $errorDetails = $null + + <# + The returned object property ExtendedErrors is an array + so that needs to be concatenated. Check if it has actual + content before using it. + #> + if (($invokeCimMethodResult | Get-Member -Name 'ExtendedErrors') -and $invokeCimMethodResult.ExtendedErrors) { - <# - The returned object property ExtendedErrors is an array - so that needs to be concatenated. - #> - $errorMessage = $invokeCimMethodResult.ExtendedErrors -join ';' + $errorDetails = $invokeCimMethodResult.ExtendedErrors -join ';' } - else + + # Fall back to Error property if ExtendedErrors was empty or not present. + if (-not $errorDetails -and ($invokeCimMethodResult | Get-Member -Name 'Error') -and $invokeCimMethodResult.Error) { - $errorMessage = $invokeCimMethodResult.Error + $errorDetails = $invokeCimMethodResult.Error } - $errorMessage = $script:localizedData.Invoke_RsCimMethod_FailedToInvokeMethod -f $MethodName, $errorMessage, $invokeCimMethodResult.HRESULT + # Use a fallback message if neither property had content. + if (-not $errorDetails) + { + $errorDetails = $script:localizedData.Invoke_RsCimMethod_NoErrorDetails + } + + # Try to translate the HRESULT code into a human-readable message. + $hResultMessage = Get-HResultMessage -HResult $invokeCimMethodResult.HRESULT + + $errorMessage = $script:localizedData.Invoke_RsCimMethod_FailedToInvokeMethod -f $MethodName, $errorDetails, $hResultMessage, $invokeCimMethodResult.HRESULT throw $errorMessage } diff --git a/source/Public/Deny-SqlDscServerPermission.ps1 b/source/Public/Deny-SqlDscServerPermission.ps1 index 9dda78020c..c0a6f6dcf6 100644 --- a/source/Public/Deny-SqlDscServerPermission.ps1 +++ b/source/Public/Deny-SqlDscServerPermission.ps1 @@ -38,7 +38,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - Deny-SqlDscServerPermission -Login $login -Permission ConnectSql, ViewServerState Denies the specified permissions to the login 'MyLogin'. @@ -46,7 +45,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $role = $serverInstance | Get-SqlDscRole -Name 'MyRole' - $role | Deny-SqlDscServerPermission -Permission AlterAnyDatabase -Force Denies the specified permissions to the role 'MyRole' without prompting for confirmation. diff --git a/source/Public/Get-SqlDscServerPermission.ps1 b/source/Public/Get-SqlDscServerPermission.ps1 index b1b86f5d06..cc80bd7030 100644 --- a/source/Public/Get-SqlDscServerPermission.ps1 +++ b/source/Public/Get-SqlDscServerPermission.ps1 @@ -82,7 +82,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - Get-SqlDscServerPermission -Login $login Get the permissions for the login 'MyLogin' using a Login object. @@ -90,14 +89,12 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $role = $serverInstance | Get-SqlDscRole -Name 'MyRole' - $role | Get-SqlDscServerPermission Get the permissions for the server role 'MyRole' using a ServerRole object from the pipeline. .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine - $serverInstance | Get-SqlDscLogin | Get-SqlDscServerPermission Get the permissions for all logins from the pipeline. diff --git a/source/Public/Grant-SqlDscServerPermission.ps1 b/source/Public/Grant-SqlDscServerPermission.ps1 index 987140e8b6..cf8c126d99 100644 --- a/source/Public/Grant-SqlDscServerPermission.ps1 +++ b/source/Public/Grant-SqlDscServerPermission.ps1 @@ -42,7 +42,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - Grant-SqlDscServerPermission -Login $login -Permission ConnectSql, ViewServerState Grants the specified permissions to the login 'MyLogin'. @@ -50,14 +49,12 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $role = $serverInstance | Get-SqlDscRole -Name 'MyRole' - $role | Grant-SqlDscServerPermission -Permission AlterAnyDatabase -WithGrant -Force Grants the specified permissions with grant option to the role 'MyRole' without prompting for confirmation. .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine - $serverInstance | Get-SqlDscLogin | Grant-SqlDscServerPermission -Permission ConnectSql Grants ConnectSql permission to all logins from the pipeline. diff --git a/source/Public/New-SqlDscDatabase.ps1 b/source/Public/New-SqlDscDatabase.ps1 index 63f45a771b..d2bbcd0c38 100644 --- a/source/Public/New-SqlDscDatabase.ps1 +++ b/source/Public/New-SqlDscDatabase.ps1 @@ -313,13 +313,10 @@ .EXAMPLE $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' - $primaryFile = New-SqlDscDataFile -Name 'MyDatabase_Primary' -FileName 'D:\SQLData\MyDatabase.mdf' -Size 102400 -Growth 10240 -GrowthType 'KB' -IsPrimaryFile -AsSpec $primaryFileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($primaryFile) -IsDefault $true -AsSpec - $secondaryFile = New-SqlDscDataFile -Name 'MyDatabase_Secondary' -FileName 'E:\SQLData\MyDatabase.ndf' -Size 204800 -AsSpec $secondaryFileGroup = New-SqlDscFileGroup -Name 'SECONDARY' -Files @($secondaryFile) -AsSpec - $serverObject | New-SqlDscDatabase -Name 'MyDatabase' -FileGroup @($primaryFileGroup, $secondaryFileGroup) -Force Creates a new database named **MyDatabase** with custom PRIMARY and SECONDARY file groups diff --git a/source/Public/New-SqlDscDatabaseSnapshot.ps1 b/source/Public/New-SqlDscDatabaseSnapshot.ps1 index 240eb41329..ed009eeadd 100644 --- a/source/Public/New-SqlDscDatabaseSnapshot.ps1 +++ b/source/Public/New-SqlDscDatabaseSnapshot.ps1 @@ -67,10 +67,8 @@ .EXAMPLE $serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance' $sourceDb = $serverObject.Databases['MyDatabase'] - $dataFile = New-SqlDscDataFile -Name 'MyDatabase_Data' -FileName 'C:\Snapshots\MyDatabase_Data.ss' -AsSpec $fileGroup = New-SqlDscFileGroup -Name 'PRIMARY' -Files @($dataFile) -AsSpec - $serverObject | New-SqlDscDatabaseSnapshot -Name 'MyDB_Snap' -DatabaseName 'MyDatabase' -FileGroup @($fileGroup) -Force Creates a new database snapshot named **MyDB_Snap** from the source database diff --git a/source/Public/New-SqlDscFileGroup.ps1 b/source/Public/New-SqlDscFileGroup.ps1 index 5267b28e71..caff8df645 100644 --- a/source/Public/New-SqlDscFileGroup.ps1 +++ b/source/Public/New-SqlDscFileGroup.ps1 @@ -56,10 +56,10 @@ .EXAMPLE $fileGroup = New-SqlDscFileGroup -Name 'MyFileGroup' - # Later add to database Add-SqlDscFileGroup -Database $database -FileGroup $fileGroup - Creates a standalone FileGroup that can be added to a Database later. + Creates a standalone FileGroup that can be added to a Database later + using Add-SqlDscFileGroup. .EXAMPLE $fileGroupSpec = New-SqlDscFileGroup -Name 'PRIMARY' -AsSpec diff --git a/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 b/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 new file mode 100644 index 0000000000..2267e8cdc9 --- /dev/null +++ b/source/Public/Request-SqlDscRSDatabaseRightsScript.ps1 @@ -0,0 +1,145 @@ +<# + .SYNOPSIS + Generates the T-SQL script to grant database permissions for Reporting Services. + + .DESCRIPTION + Generates the T-SQL script to grant the necessary database permissions for + a user account to access the report server database. This is done by calling + the `GenerateDatabaseRightsScript` method on the + `MSReportServer_ConfigurationSetting` CIM instance. + + The generated script can be executed on a SQL Server Database Engine + instance using `Invoke-SqlDscQuery` to grant the required permissions. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER DatabaseName + Specifies the name of the report server database to grant permissions on. + This should match the database name used when creating the database. + + .PARAMETER UserName + Specifies the user account name to grant permissions to. This is typically + the Reporting Services service account. + + .PARAMETER IsRemote + If specified, indicates that the database is on a remote server. + By default, assumes the database is local. + + .PARAMETER UseSqlAuthentication + If specified, indicates the user is a SQL Server authentication user. + By default, assumes Windows authentication. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'NT SERVICE\SQLServerReportingServices' + + Generates the T-SQL script to grant permissions on the 'ReportServer' + database for the Reporting Services service account. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + $serviceAccount = $config.WindowsServiceIdentityActual + $script = $config | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $serviceAccount + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $script -Force + + Gets the Reporting Services service account from the configuration object, + generates the database rights script, and executes it on the RSDB SQL + Server instance. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Request-SqlDscRSDatabaseRightsScript -Configuration $config -DatabaseName 'ReportServer' -UserName 'DOMAIN\SQLRSUser' -IsRemote + + Generates the rights script for a remote database scenario. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + `System.String` + + Returns the T-SQL script as a string. + + .NOTES + This command should be run after creating the report server database + using the script from `Request-SqlDscRSDatabaseScript`. After granting + permissions, use `Set-SqlDscRSDatabaseConnection` to configure the + Reporting Services instance to use the database. + + To get the Reporting Services service account name, use the + `WindowsServiceIdentityActual` property from the configuration object: + `(Get-SqlDscRSConfiguration -InstanceName 'SSRS').WindowsServiceIdentityActual` + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-generatedatabaserightsscript +#> +function Request-SqlDscRSDatabaseRightsScript +{ + # cSpell: ignore PBIRS + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.String] + $DatabaseName, + + [Parameter(Mandatory = $true)] + [System.String] + $UserName, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $IsRemote, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $UseSqlAuthentication + ) + + process + { + $rsInstanceName = $Configuration.InstanceName + + Write-Verbose -Message ($script:localizedData.Request_SqlDscRSDatabaseRightsScript_Generating -f $DatabaseName, $UserName, $rsInstanceName) + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'GenerateDatabaseRightsScript' + Arguments = @{ + DatabaseName = $DatabaseName + UserName = $UserName + IsRemote = $IsRemote.IsPresent + IsWindowsUser = -not $UseSqlAuthentication.IsPresent + } + } + + try + { + $result = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Request_SqlDscRSDatabaseRightsScript_FailedToGenerate -f $rsInstanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'RSRDBRS0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + return $result.Script + } +} diff --git a/source/Public/Request-SqlDscRSDatabaseScript.ps1 b/source/Public/Request-SqlDscRSDatabaseScript.ps1 new file mode 100644 index 0000000000..a8d4591b28 --- /dev/null +++ b/source/Public/Request-SqlDscRSDatabaseScript.ps1 @@ -0,0 +1,140 @@ +<# + .SYNOPSIS + Generates the T-SQL script to create a report server database. + + .DESCRIPTION + Generates the T-SQL script to create a report server database for SQL + Server Reporting Services or Power BI Report Server by calling the + `GenerateDatabaseCreationScript` method on the + `MSReportServer_ConfigurationSetting` CIM instance. + + The generated script can be executed on a SQL Server Database Engine + instance using `Invoke-SqlDscQuery` to create the report server database. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + The Reporting Services service must be running before calling this command. + For SQL Server Reporting Services the service name is `SQLServerReportingServices`, + and for Power BI Report Server the service name is `PowerBIReportServer`. + If the service is not running, a terminating error is thrown. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER DatabaseName + Specifies the name of the report server database to create. Common names + are 'ReportServer' or 'ReportServer$InstanceName'. + + .PARAMETER Lcid + Specifies the Language Code ID (LCID) to use for the database collation. + If not specified, defaults to the operating system language. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' + + Generates the T-SQL script to create the 'ReportServer' database for + the 'SSRS' Reporting Services instance. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + $script = $config | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $script -Force + + Generates the database creation script and executes it on the RSDB + SQL Server instance to create the report server database. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + `System.String` + + Returns the T-SQL script as a string. + + .NOTES + After creating the database, use `Request-SqlDscRSDatabaseRightsScript` + to generate the script that grants the necessary permissions, then use + `Set-SqlDscRSDatabaseConnection` to configure the Reporting Services + instance to use the new database. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-generatedatabasecreationscript +#> +function Request-SqlDscRSDatabaseScript +{ + # cSpell: ignore PBIRS Lcid + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.String] + $DatabaseName, + + [Parameter()] + [System.Int32] + $Lcid + ) + + process + { + $rsInstanceName = $Configuration.InstanceName + $serviceName = $Configuration.ServiceName + + $service = Get-Service -Name $serviceName -ErrorAction 'SilentlyContinue' + + if (-not $service -or $service.Status -ne 'Running') + { + $errorMessage = $script:localizedData.Request_SqlDscRSDatabaseScript_ServiceNotRunning -f $serviceName, $rsInstanceName + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'RSRDBS0002' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + if (-not $PSBoundParameters.ContainsKey('Lcid')) + { + $Lcid = (Get-OperatingSystem).OSLanguage + } + + Write-Verbose -Message ($script:localizedData.Request_SqlDscRSDatabaseScript_Generating -f $DatabaseName, $rsInstanceName) + + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'GenerateDatabaseCreationScript' + Arguments = @{ + DatabaseName = $DatabaseName + # IsSharePointMode must always be false. SharePoint integrated mode + # is not supported since SQL Server 2012 (11.x) via WMI provider. + IsSharePointMode = $false + Lcid = $Lcid + } + } + + try + { + $result = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Request_SqlDscRSDatabaseScript_FailedToGenerate -f $rsInstanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'RSRDBS0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + return $result.Script + } +} diff --git a/source/Public/Revoke-SqlDscServerPermission.ps1 b/source/Public/Revoke-SqlDscServerPermission.ps1 index 220b5f8ef5..14f1a75a24 100644 --- a/source/Public/Revoke-SqlDscServerPermission.ps1 +++ b/source/Public/Revoke-SqlDscServerPermission.ps1 @@ -43,7 +43,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - Revoke-SqlDscServerPermission -Login $login -Permission ConnectSql, ViewServerState Revokes the specified permissions from the login 'MyLogin'. @@ -51,7 +50,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $role = $serverInstance | Get-SqlDscRole -Name 'MyRole' - $role | Revoke-SqlDscServerPermission -Permission AlterAnyDatabase -WithGrant -Force Revokes the specified permissions and the right to grant them from the role 'MyRole' with cascading effect, without prompting for confirmation. diff --git a/source/Public/Set-SqlDscDatabasePermission.ps1 b/source/Public/Set-SqlDscDatabasePermission.ps1 index a0f53d6b7a..9a433fd76e 100644 --- a/source/Public/Set-SqlDscDatabasePermission.ps1 +++ b/source/Public/Set-SqlDscDatabasePermission.ps1 @@ -41,12 +41,10 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine - $setPermission = [Microsoft.SqlServer.Management.Smo.DatabasePermissionSet] @{ Connect = $true Update = $true } - Set-SqlDscDatabasePermission -ServerObject $serverInstance -DatabaseName 'MyDatabase' -Name 'MyPrincipal' -State 'Grant' -Permission $setPermission Sets the permissions for the principal 'MyPrincipal'. diff --git a/source/Public/Set-SqlDscRSDatabaseConnection.ps1 b/source/Public/Set-SqlDscRSDatabaseConnection.ps1 new file mode 100644 index 0000000000..c2f439438a --- /dev/null +++ b/source/Public/Set-SqlDscRSDatabaseConnection.ps1 @@ -0,0 +1,247 @@ +<# + .SYNOPSIS + Sets the report server database connection for SQL Server Reporting Services. + + .DESCRIPTION + Sets the report server database connection for SQL Server Reporting Services + or Power BI Report Server by calling the `SetDatabaseConnection` method on + the `MSReportServer_ConfigurationSetting` CIM instance. + + This command configures which database the report server should use for + storing report definitions, metadata, and other report server data. + + The configuration CIM instance can be obtained using the + `Get-SqlDscRSConfiguration` command and passed via the pipeline. + + .PARAMETER Configuration + Specifies the `MSReportServer_ConfigurationSetting` CIM instance for + the Reporting Services instance. This can be obtained using the + `Get-SqlDscRSConfiguration` command. This parameter accepts pipeline + input. + + .PARAMETER ServerName + Specifies the name of the server that hosts the report server database. + + .PARAMETER InstanceName + Specifies the name of the SQL Server instance that hosts the report + server database. If not specified, the default instance is used. + + .PARAMETER DatabaseName + Specifies the name of the report server database. Common names are + 'ReportServer' or 'ReportServer$InstanceName'. + + .PARAMETER Type + Specifies the type of credentials to use for the database connection. + Valid values are: + - 'Windows': Windows authentication with specified credentials. + - 'SqlServer': SQL Server authentication with specified credentials. + - 'ServiceAccount': Windows Service integrated security using the + report server service account. + + The default is 'ServiceAccount'. + + .PARAMETER Credential + Specifies the credentials for connecting to the database when using + 'Windows' or 'SqlServer' credentials type. This parameter is required + when Type is 'Windows' or 'SqlServer'. + + .PARAMETER PassThru + If specified, returns the configuration CIM instance after setting + the database connection. + + .PARAMETER Force + If specified, suppresses the confirmation prompt. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' + + Sets the report server database connection to use the 'ReportServer' + database on 'localhost' using the report server service account. + + .EXAMPLE + $config = Get-SqlDscRSConfiguration -InstanceName 'SSRS' + Set-SqlDscRSDatabaseConnection -Configuration $config -ServerName 'SqlServer01' -InstanceName 'MSSQLSERVER' -DatabaseName 'ReportServer' -Confirm:$false + + Sets the report server database connection without confirmation. + + .EXAMPLE + $credential = Get-Credential + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSDatabaseConnection -ServerName 'SqlServer01' -DatabaseName 'ReportServer' -Type 'SqlServer' -Credential $credential + + Sets the report server database connection using SQL Server + authentication with the specified credentials. + + .EXAMPLE + Get-SqlDscRSConfiguration -InstanceName 'SSRS' | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -PassThru + + Sets the database connection and returns the configuration CIM instance. + + .INPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + Accepts MSReportServer_ConfigurationSetting CIM instance via pipeline. + + .OUTPUTS + None. By default, this command does not generate any output. + + .OUTPUTS + `Microsoft.Management.Infrastructure.CimInstance` + + When PassThru is specified, returns the MSReportServer_ConfigurationSetting + CIM instance. + + .NOTES + The Reporting Services service may need to be restarted for the change + to take effect. + + When using Type 'ServiceAccount', the report server web service + will use either the ASP.NET account or an application pool's account and + the Windows service account to access the report server database. + + .LINK + https://docs.microsoft.com/en-us/sql/reporting-services/wmi-provider-library-reference/configurationsetting-method-setdatabaseconnection +#> +function Set-SqlDscRSDatabaseConnection +{ + # cSpell: ignore PBIRS + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples use pipeline input the rule cannot validate.')] + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + [OutputType([System.Object])] + param + ( + [Parameter(Mandatory = $true, ValueFromPipeline = $true)] + [System.Object] + $Configuration, + + [Parameter(Mandatory = $true)] + [System.String] + $ServerName, + + [Parameter()] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + $DatabaseName, + + [Parameter()] + [ValidateSet('Windows', 'SqlServer', 'ServiceAccount')] + [System.String] + $Type = 'ServiceAccount', + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $PassThru, + + [Parameter()] + [System.Management.Automation.SwitchParameter] + $Force + ) + + process + { + if ($Force.IsPresent -and -not $Confirm) + { + $ConfirmPreference = 'None' + } + + $rsInstanceName = $Configuration.InstanceName + + # Build the database server connection string + if ($PSBoundParameters.ContainsKey('InstanceName')) + { + $databaseServerName = '{0}\{1}' -f $ServerName, $InstanceName + } + else + { + $databaseServerName = $ServerName + } + + # Validate Credential parameter is provided when required + if ($Type -in @('Windows', 'SqlServer') -and -not $PSBoundParameters.ContainsKey('Credential')) + { + $errorMessage = $script:localizedData.Set_SqlDscRSDatabaseConnection_CredentialRequired -f $Type + + $errorRecord = New-ErrorRecord -Exception (New-ArgumentException -Message $errorMessage -ArgumentName 'Credential' -PassThru) -ErrorId 'SSRSDC0002' -ErrorCategory 'InvalidArgument' -TargetObject $Type + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + + <# + Map Type to the numeric value expected by the CIM method: + 0 = Windows + 1 = SQL Server + 2 = Windows Service (Integrated Security) + #> + $typeNumeric = switch ($Type) + { + 'Windows' + { + 0 + } + + 'SqlServer' + { + 1 + } + + 'ServiceAccount' + { + 2 + } + } + + $userName = '' + $password = '' + + if ($PSBoundParameters.ContainsKey('Credential')) + { + $userName = $Credential.UserName + $password = $Credential.GetNetworkCredential().Password + } + + Write-Verbose -Message ($script:localizedData.Set_SqlDscRSDatabaseConnection_Setting -f $DatabaseName, $databaseServerName, $rsInstanceName) + + $descriptionMessage = $script:localizedData.Set_SqlDscRSDatabaseConnection_ShouldProcessDescription -f $DatabaseName, $databaseServerName, $rsInstanceName + $confirmationMessage = $script:localizedData.Set_SqlDscRSDatabaseConnection_ShouldProcessConfirmation -f $DatabaseName, $databaseServerName + $captionMessage = $script:localizedData.Set_SqlDscRSDatabaseConnection_ShouldProcessCaption + + if ($PSCmdlet.ShouldProcess($descriptionMessage, $confirmationMessage, $captionMessage)) + { + $invokeRsCimMethodParameters = @{ + CimInstance = $Configuration + MethodName = 'SetDatabaseConnection' + Arguments = @{ + Server = $databaseServerName + DatabaseName = $DatabaseName + Username = $userName + Password = $password + CredentialsType = $typeNumeric + } + } + + try + { + $null = Invoke-RsCimMethod @invokeRsCimMethodParameters -ErrorAction 'Stop' + } + catch + { + $errorMessage = $script:localizedData.Set_SqlDscRSDatabaseConnection_FailedToSet -f $rsInstanceName, $_.Exception.Message + + $errorRecord = New-ErrorRecord -Exception (New-InvalidOperationException -Message $errorMessage -PassThru) -ErrorId 'SSRSDC0001' -ErrorCategory 'InvalidOperation' -TargetObject $Configuration + + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + if ($PassThru.IsPresent) + { + return $Configuration + } + } +} diff --git a/source/Public/Set-SqlDscServerPermission.ps1 b/source/Public/Set-SqlDscServerPermission.ps1 index 0d3589645e..aeeb545066 100644 --- a/source/Public/Set-SqlDscServerPermission.ps1 +++ b/source/Public/Set-SqlDscServerPermission.ps1 @@ -60,7 +60,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - Set-SqlDscServerPermission -Login $login -Grant ConnectSql, ViewServerState Sets the exact granted permissions for the login 'MyLogin'. Any other @@ -69,7 +68,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - Set-SqlDscServerPermission -Login $login -Grant ConnectSql -GrantWithGrant AlterAnyDatabase -Deny ViewAnyDatabase Sets exact permissions for the login 'MyLogin': grants ConnectSql, @@ -79,7 +77,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $role = $serverInstance | Get-SqlDscRole -Name 'MyRole' - $role | Set-SqlDscServerPermission -Grant @() -Force Revokes all granted permissions from the role 'MyRole' without prompting diff --git a/source/Public/Test-SqlDscServerPermission.ps1 b/source/Public/Test-SqlDscServerPermission.ps1 index 3861a37c0e..ad092a8fc3 100644 --- a/source/Public/Test-SqlDscServerPermission.ps1 +++ b/source/Public/Test-SqlDscServerPermission.ps1 @@ -57,7 +57,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - $isInDesiredState = Test-SqlDscServerPermission -Login $login -Grant -Permission ConnectSql, ViewServerState Tests if the specified permissions are granted to the login 'MyLogin'. @@ -65,7 +64,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $role = $serverInstance | Get-SqlDscRole -Name 'MyRole' - $isInDesiredState = $role | Test-SqlDscServerPermission -Grant -Permission AlterAnyDatabase -WithGrant Tests if the specified permissions are granted with grant option to the role 'MyRole'. @@ -73,7 +71,6 @@ .EXAMPLE $serverInstance = Connect-SqlDscDatabaseEngine $login = $serverInstance | Get-SqlDscLogin -Name 'MyLogin' - $isInDesiredState = Test-SqlDscServerPermission -Login $login -Grant -Permission @() Tests if the login 'MyLogin' has no permissions granted (empty permission set). diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index f30f6e634b..a577168d1a 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -321,7 +321,20 @@ ConvertFrom-StringData @' Disable_SqlDscRsSecureConnection_FailedToDisable = Failed to disable secure connection for Reporting Services instance '{0}'. {1} (DSRSSC0001) ## Invoke-RsCimMethod - Invoke_RsCimMethod_FailedToInvokeMethod = Method {0}() failed with an error. Error: {1} (HRESULT:{2}) (IRCM0001) + Invoke_RsCimMethod_FailedToInvokeMethod = Method {0}() failed with an error. Error: {1} {2} (HRESULT:{3}) (IRCM0001) + 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) + HResult_InvalidArgument = One or more arguments are not valid. (GHRM0004) + HResult_OutOfMemory = The system is out of memory. (GHRM0005) + HResult_RpcDisconnected = The object invoked has disconnected from its clients or the RPC connection was lost. Verify that the Reporting Services service is running. (GHRM0006) + HResult_RpcServerUnavailable = The RPC server is unavailable. Verify that the Reporting Services service is running and accessible. (GHRM0007) + HResult_ServiceNotActive = The service has not been started. Verify that the Reporting Services service is running. (GHRM0008) + HResult_Unknown = Unknown HRESULT code {0}. Refer to Microsoft documentation for more information. (GHRM0009) ## Test-SqlDscRSInstalled Test_SqlDscRSInstalled_Checking = Checking if Reporting Services instance '{0}' is installed. @@ -756,4 +769,22 @@ ConvertFrom-StringData @' # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. Set_SqlDscRSVirtualDirectory_ShouldProcessCaption = Set virtual directory for Reporting Services instance Set_SqlDscRSVirtualDirectory_FailedToSet = Failed to set virtual directory for Reporting Services instance '{0}'. {1} (SSRSVD0001) + + ## Request-SqlDscRSDatabaseScript + Request_SqlDscRSDatabaseScript_Generating = Generating database creation script for database '{0}' for Reporting Services instance '{1}'. + Request_SqlDscRSDatabaseScript_FailedToGenerate = Failed to generate database creation script for Reporting Services instance '{0}'. {1} (RSRDBS0001) + Request_SqlDscRSDatabaseScript_ServiceNotRunning = The Reporting Services service '{0}' for instance '{1}' is not running. Start the service before generating the database creation script. (RSRDBS0002) + + ## Request-SqlDscRSDatabaseRightsScript + Request_SqlDscRSDatabaseRightsScript_Generating = Generating database rights script for database '{0}' and user '{1}' for Reporting Services instance '{2}'. + Request_SqlDscRSDatabaseRightsScript_FailedToGenerate = Failed to generate database rights script for Reporting Services instance '{0}'. {1} (RSRDBRS0001) + + ## Set-SqlDscRSDatabaseConnection + Set_SqlDscRSDatabaseConnection_Setting = Setting report server database connection to database '{0}' on server '{1}' for Reporting Services instance '{2}'. + Set_SqlDscRSDatabaseConnection_ShouldProcessDescription = Setting report server database connection to database '{0}' on server '{1}' for Reporting Services instance '{2}'. + Set_SqlDscRSDatabaseConnection_ShouldProcessConfirmation = Are you sure you want to set the report server database connection to database '{0}' on server '{1}'? + # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. + Set_SqlDscRSDatabaseConnection_ShouldProcessCaption = Set report server database connection for Reporting Services instance + Set_SqlDscRSDatabaseConnection_FailedToSet = Failed to set report server database connection for Reporting Services instance '{0}'. {1} (SSRSDC0001) + Set_SqlDscRSDatabaseConnection_CredentialRequired = The Credential parameter is required when Type is '{0}'. (SSRSDC0002) '@ diff --git a/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 b/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 index e2c5e72775..f513e449e5 100644 --- a/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Prerequisites.Integration.Tests.ps1 @@ -33,7 +33,7 @@ BeforeAll { # CSpell: ignore Remoting Describe 'Prerequisites' { - Context 'Create required local Windows users' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI') { + Context 'Create required local Windows users' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI', 'Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { BeforeAll { $password = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force } @@ -60,7 +60,7 @@ Describe 'Prerequisites' { } } - Context 'Should create required local Windows service accounts' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI') { + Context 'Should create required local Windows service accounts' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI', 'Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { BeforeAll { $password = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force } @@ -94,7 +94,7 @@ Describe 'Prerequisites' { } } - Context 'Create required local Windows groups' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI') { + Context 'Create required local Windows groups' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022', 'Integration_PowerBI', 'Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS') { It 'Should create SqlIntegrationTestGroup group' { $group = New-LocalGroup -Name 'SqlIntegrationTestGroup' -Description 'Local Windows group for SQL integration testing.' diff --git a/tests/Integration/Commands/Prerequisites.RSDB.Integration.Tests.ps1 b/tests/Integration/Commands/Prerequisites.RSDB.Integration.Tests.ps1 new file mode 100644 index 0000000000..13308ce48d --- /dev/null +++ b/tests/Integration/Commands/Prerequisites.RSDB.Integration.Tests.ps1 @@ -0,0 +1,92 @@ +[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' +} + +# cSpell: ignore RSDB +Describe 'Prerequisites - RSDB SQL Server Instance' -Tag @('Integration_SQL2017_RS', 'Integration_SQL2019_RS', 'Integration_SQL2022_RS', 'Integration_PowerBI') { + BeforeAll { + Write-Verbose -Message ('Running integration test as user ''{0}''.' -f $env:UserName) -Verbose + + $computerName = Get-ComputerName + } + + Context 'Install SQL Server Database Engine instance RSDB for Reporting Services database' { + It 'Should install the RSDB instance without throwing' { + <# + Install a minimal SQL Server Database Engine instance named RSDB + that will be used to host the Reporting Services database for + integration tests. + + This reuses the same accounts and passwords as the main Prerequisites + test file. The media path ($env:IsoDrivePath) is set by the main + Prerequisites test that runs before this one. + #> + $installSqlDscServerParameters = @{ + Install = $true + AcceptLicensingTerms = $true + InstanceName = 'RSDB' + Features = 'SQLENGINE' + SqlSysAdminAccounts = @( + ('{0}\SqlAdmin' -f $computerName) + 'BUILTIN\Administrators' + ) + SqlSvcAccount = '{0}\svc-SqlPrimary' -f $computerName + SqlSvcPassword = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force + SqlSvcStartupType = 'Automatic' + AgtSvcAccount = '{0}\svc-SqlAgentPri' -f $computerName + AgtSvcPassword = ConvertTo-SecureString -String 'yig-C^Equ3' -AsPlainText -Force + AgtSvcStartupType = 'Automatic' + BrowserSvcStartupType = 'Automatic' + SecurityMode = 'SQL' + SAPwd = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force + SqlCollation = 'SQL_Latin1_General_CP1_CI_AS' + InstallSharedDir = 'C:\Program Files\Microsoft SQL Server' + InstallSharedWOWDir = 'C:\Program Files (x86)\Microsoft SQL Server' + NpEnabled = $true + TcpEnabled = $true + MediaPath = $env:IsoDrivePath + Verbose = $true + ErrorAction = 'Stop' + Force = $true + } + + Install-SqlDscServer @installSqlDscServerParameters + } + + It 'Should have the RSDB instance running' { + $service = Get-Service -Name 'MSSQL$RSDB' -ErrorAction 'SilentlyContinue' + + $service | Should -Not -BeNullOrEmpty + $service.Status | Should -Be 'Running' + } + } +} diff --git a/tests/Integration/Commands/README.md b/tests/Integration/Commands/README.md index 3016c2825a..b4e28f4e60 100644 --- a/tests/Integration/Commands/README.md +++ b/tests/Integration/Commands/README.md @@ -158,6 +158,7 @@ Tests for SQL Server Reporting Services commands. Command | Run order # | Depends on # | Use instance | Creates persistent objects --- | --- | --- | --- | --- Prerequisites | 0 | - | - | Sets up dependencies +Prerequisites.RSDB | 0 | - | - | Installs RSDB SQL Server instance for RS database tests Save-SqlDscSqlServerMediaFile | 0 | - | - | Downloads SQL Server media files Import-SqlDscPreferredModule | 0 | - | - | - Install-SqlDscReportingService | 1 | 0 (Prerequisites) | - | SSRS instance @@ -165,6 +166,8 @@ Get-SqlDscInstalledInstance | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequ 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 | - +Request-SqlDscRSDatabaseScript | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Request-SqlDscRSDatabaseRightsScript | 2 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Get-SqlDscRSConfiguration | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Enable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Disable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - @@ -172,6 +175,7 @@ Get-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequi Add-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Remove-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - Set-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscReportingService), 0 (Prerequisites) | SSRS | - +Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscReportingService), 0 (Prerequisites, Prerequisites.RSDB) | SSRS, RSDB | ReportServer, ReportServerTempDB databases Repair-SqlDscReportingService | 8 | 1 (Install-SqlDscReportingService) | SSRS | - Uninstall-SqlDscReportingService | 9 | 8 (Repair-SqlDscReportingService) | - | - @@ -184,6 +188,7 @@ Tests for Power BI Report Server commands. Command | Run order # | Depends on # | Use instance | Creates persistent objects --- | --- | --- | --- | --- Prerequisites | 0 | - | - | Sets up dependencies +Prerequisites.RSDB | 0 | - | - | Installs RSDB SQL Server instance for RS database tests Save-SqlDscSqlServerMediaFile | 0 | - | - | Downloads SQL Server media files Import-SqlDscPreferredModule | 0 | - | - | - Install-SqlDscPowerBIReportServer | 1 | 0 (Prerequisites) | - | PBIRS instance @@ -191,6 +196,8 @@ Get-SqlDscInstalledInstance | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prer 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 | - +Request-SqlDscRSDatabaseScript | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Request-SqlDscRSDatabaseRightsScript | 2 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Get-SqlDscRSConfiguration | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Enable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Disable-SqlDscRsSecureConnection | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - @@ -198,6 +205,7 @@ Get-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prere Add-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Remove-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - Set-SqlDscRSUrlReservation | 3 | 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites) | PBIRS | - +Set-SqlDscRSDatabaseConnection | 3 | 2 (Request-SqlDscRSDatabaseScript, Request-SqlDscRSDatabaseRightsScript), 1 (Install-SqlDscPowerBIReportServer), 0 (Prerequisites, Prerequisites.RSDB) | PBIRS, RSDB | ReportServer, ReportServerTempDB databases Repair-SqlDscPowerBIReportServer | 8 | 1 (Install-SqlDscPowerBIReportServer) | PBIRS | - Uninstall-SqlDscPowerBIReportServer | 9 | 8 (Repair-SqlDscPowerBIReportServer) | - | - @@ -266,7 +274,7 @@ These instances is available for integration tests. Instance | Feature | State --- | --- | --- -DSCSQLTEST | SQLENGINE | Running +DSCSQLTEST | SQLENGINE | Running MSSQLSERVER | SQLENGINE | Stopped All running Database Engine instances also have a SQL Server Agent that is started. @@ -278,6 +286,23 @@ both Named Pipes and TCP/IP protocol enabled. > Some services are stopped to save memory on the build worker. See the > column _State_. +#### Reporting Services Instances + +These additional instances are available for Reporting Services integration tests. + + + +Instance | Feature | State | Description +--- | --- | --- | --- +RSDB | SQLENGINE | Running | Hosts report server databases for SSRS/PBIRS configuration tests +SSRS | ReportingServices | Running | SQL Server Reporting Services instance +PBIRS | ReportingServices | Running | Power BI Report Server instance + + +> [!NOTE] +> The RSDB instance is installed by the `Prerequisites.RSDB.Integration.Tests.ps1` +> and is only available in the ReportingServices and BIReportServer test stages. + #### Instance properties - **Collation:** Finnish\_Swedish\_CI\_AS diff --git a/tests/Integration/Commands/Request-SqlDscRSDatabaseRightsScript.Integration.Tests.ps1 b/tests/Integration/Commands/Request-SqlDscRSDatabaseRightsScript.Integration.Tests.ps1 new file mode 100644 index 0000000000..082d0424a9 --- /dev/null +++ b/tests/Integration/Commands/Request-SqlDscRSDatabaseRightsScript.Integration.Tests.ps1 @@ -0,0 +1,119 @@ +[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 'Request-SqlDscRSDatabaseRightsScript' { + Context 'When generating database rights script for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Import the preferred module to ensure SMO types are loaded. + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + } + + It 'Should generate the database rights script without throwing' { + { $script:rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:rightsScript | Should -Not -BeNullOrEmpty + # The script should contain permission grants + $script:rightsScript | Should -Match 'GRANT|CREATE USER|ALTER' + } + } + + Context 'When generating database rights script for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Import the preferred module to ensure SMO types are loaded. + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + } + + It 'Should generate the database rights script without throwing' { + { $script:rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:rightsScript | Should -Not -BeNullOrEmpty + $script:rightsScript | Should -Match 'GRANT|CREATE USER|ALTER' + } + } + + Context 'When generating database rights script for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Import the preferred module to ensure SMO types are loaded. + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + } + + It 'Should generate the database rights script without throwing' { + { $script:rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:rightsScript | Should -Not -BeNullOrEmpty + $script:rightsScript | Should -Match 'GRANT|CREATE USER|ALTER' + } + } + + Context 'When generating database rights script for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + + # Import the preferred module to ensure SMO types are loaded. + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + # Get the Power BI Report Server service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + } + + It 'Should generate the database rights script without throwing' { + { $script:rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:rightsScript | Should -Not -BeNullOrEmpty + $script:rightsScript | Should -Match 'GRANT|CREATE USER|ALTER' + } + } +} diff --git a/tests/Integration/Commands/Request-SqlDscRSDatabaseScript.Integration.Tests.ps1 b/tests/Integration/Commands/Request-SqlDscRSDatabaseScript.Integration.Tests.ps1 new file mode 100644 index 0000000000..3a59bcc252 --- /dev/null +++ b/tests/Integration/Commands/Request-SqlDscRSDatabaseScript.Integration.Tests.ps1 @@ -0,0 +1,150 @@ +[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 'Request-SqlDscRSDatabaseScript' { + Context 'When generating database creation script for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + <# + Ensure the Reporting Services service is running before running + tests. The service may have been stopped by a previous test to + save memory on the build worker. + #> + $service = Get-Service -Name 'SQLServerReportingServices' -ErrorAction 'SilentlyContinue' + + if ($service -and $service.Status -ne 'Running') + { + Write-Verbose -Message 'Starting SQLServerReportingServices service...' -Verbose + + Start-Service -Name 'SQLServerReportingServices' -ErrorAction 'Stop' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should generate the database creation script without throwing' { + { $script:databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:databaseScript | Should -Not -BeNullOrEmpty + $script:databaseScript | Should -Match 'CREATE DATABASE' + } + } + + Context 'When generating database creation script for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + <# + Ensure the Reporting Services service is running before running + tests. The service may have been stopped by a previous test to + save memory on the build worker. + #> + $service = Get-Service -Name 'SQLServerReportingServices' -ErrorAction 'SilentlyContinue' + + if ($service -and $service.Status -ne 'Running') + { + Write-Verbose -Message 'Starting SQLServerReportingServices service...' -Verbose + + Start-Service -Name 'SQLServerReportingServices' -ErrorAction 'Stop' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should generate the database creation script without throwing' { + { $script:databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:databaseScript | Should -Not -BeNullOrEmpty + $script:databaseScript | Should -Match 'CREATE DATABASE' + } + } + + Context 'When generating database creation script for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + <# + Ensure the Reporting Services service is running before running + tests. The service may have been stopped by a previous test to + save memory on the build worker. + #> + $service = Get-Service -Name 'SQLServerReportingServices' -ErrorAction 'SilentlyContinue' + + if ($service -and $service.Status -ne 'Running') + { + Write-Verbose -Message 'Starting SQLServerReportingServices service...' -Verbose + + Start-Service -Name 'SQLServerReportingServices' -ErrorAction 'Stop' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + } + + It 'Should generate the database creation script without throwing' { + { $script:databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:databaseScript | Should -Not -BeNullOrEmpty + $script:databaseScript | Should -Match 'CREATE DATABASE' + } + } + + Context 'When generating database creation script for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + <# + Ensure the Power BI Report Server service is running before + running tests. The service may have been stopped by a previous + test to save memory on the build worker. + #> + $service = Get-Service -Name 'PowerBIReportServer' -ErrorAction 'SilentlyContinue' + + if ($service -and $service.Status -ne 'Running') + { + Write-Verbose -Message 'Starting PowerBIReportServer service...' -Verbose + + Start-Service -Name 'PowerBIReportServer' -ErrorAction 'Stop' + } + + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + } + + It 'Should generate the database creation script without throwing' { + { $script:databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' } | Should -Not -Throw + } + + It 'Should return a string containing T-SQL' { + $script:databaseScript | Should -Not -BeNullOrEmpty + $script:databaseScript | Should -Match 'CREATE DATABASE' + } + } +} diff --git a/tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1 b/tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1 new file mode 100644 index 0000000000..f41375e119 --- /dev/null +++ b/tests/Integration/Commands/Set-SqlDscRSDatabaseConnection.Integration.Tests.ps1 @@ -0,0 +1,166 @@ +[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' +} + +# cSpell: ignore RSDB +Describe 'Set-SqlDscRSDatabaseConnection' { + Context 'When setting database connection for SQL Server Reporting Services' -Tag @('Integration_SQL2017_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + <# + Create the report server database on the RSDB SQL Server instance. + This follows the workflow: + 1. Generate database creation script + 2. Execute script using Invoke-SqlDscQuery + 3. Generate database rights script + 4. Execute rights script using Invoke-SqlDscQuery + #> + + # Generate and execute database creation script + $databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' + + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $databaseScript -Force -ErrorAction 'Stop' + + # Generate and execute database rights script + $rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $rightsScript -Force -ErrorAction 'Stop' + } + + It 'Should set the database connection' { + $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -Force -ErrorAction 'Stop' + } + + It 'Should return the configuration when using PassThru' { + $result = $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When setting database connection for SQL Server 2019 Reporting Services' -Tag @('Integration_SQL2019_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + $databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' + + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $databaseScript -Force -ErrorAction 'Stop' + + $rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $rightsScript -Force -ErrorAction 'Stop' + } + + It 'Should set the database connection' { + $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -Force -ErrorAction 'Stop' + } + + It 'Should return the configuration when using PassThru' { + $result = $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When setting database connection for SQL Server 2022 Reporting Services' -Tag @('Integration_SQL2022_RS') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'SSRS' -ErrorAction 'Stop' + + # Get the Reporting Services service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + $databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' + + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $databaseScript -Force -ErrorAction 'Stop' + + $rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $rightsScript -Force -ErrorAction 'Stop' + } + + It 'Should set the database connection' { + $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -Force -ErrorAction 'Stop' + } + + It 'Should return the configuration when using PassThru' { + $result = $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When setting database connection for Power BI Report Server' -Tag @('Integration_PowerBI') { + BeforeAll { + $script:configuration = Get-SqlDscRSConfiguration -InstanceName 'PBIRS' -ErrorAction 'Stop' + + # Get the Power BI Report Server service account from the configuration object. + $script:serviceAccount = $script:configuration.WindowsServiceIdentityActual + + $databaseScript = $script:configuration | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -ErrorAction 'Stop' + + Import-SqlDscPreferredModule -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $databaseScript -Force -ErrorAction 'Stop' + + $rightsScript = $script:configuration | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName $script:serviceAccount -ErrorAction 'Stop' + + Invoke-SqlDscQuery -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'master' -Query $rightsScript -Force -ErrorAction 'Stop' + } + + It 'Should set the database connection' { + $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -Force -ErrorAction 'Stop' + } + + It 'Should return the configuration when using PassThru' { + $result = $script:configuration | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -InstanceName 'RSDB' -DatabaseName 'ReportServer' -PassThru -Force -ErrorAction 'Stop' + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'PBIRS' + } + } +} diff --git a/tests/QA/ScriptAnalyzer.Tests.ps1 b/tests/QA/ScriptAnalyzer.Tests.ps1 index 1fda880a42..991ce9e8ff 100644 --- a/tests/QA/ScriptAnalyzer.Tests.ps1 +++ b/tests/QA/ScriptAnalyzer.Tests.ps1 @@ -45,6 +45,7 @@ BeforeDiscovery { $referencedAssemblies = @( 'System.Collections' 'System.Collections.Specialized' + 'System.ComponentModel.TypeConverter' 'System.Data.Common' 'System.Linq' 'System.Net.Primitives' diff --git a/tests/QA/module.tests.ps1 b/tests/QA/module.tests.ps1 index b630b377c9..530f2e3380 100644 --- a/tests/QA/module.tests.ps1 +++ b/tests/QA/module.tests.ps1 @@ -232,6 +232,103 @@ Describe 'Comment-based help structure' -Tags 'helpQuality' { $invalidDirectives | Should -BeNullOrEmpty -Because ('invalid help directives found that will break help parsing: {0}' -f ($invalidDirectives -join ', ')) } } + + It 'Should not have comments within multi-line example code blocks for ' { + <# + PlatyPS expects .EXAMPLE blocks to have: code (no comments) → blank line → description. + Comments (lines starting with #) within the code portion cause "Expect Heading" errors + during documentation generation because PlatyPS interprets them incorrectly. + #> + if ($scriptFileRawContent -match '(?s)<#(.*?)#>') + { + $helpBlock = $Matches[1] + + # Find all .EXAMPLE blocks + $exampleMatches = [regex]::Matches($helpBlock, '(?s)\.EXAMPLE\s*\r?\n(.*?)(?=\r?\n\s*\.(?:EXAMPLE|PARAMETER|SYNOPSIS|DESCRIPTION|INPUTS|OUTPUTS|NOTES|LINK|COMPONENT|ROLE|FUNCTIONALITY)|$)') + + $examplesWithComments = @() + + foreach ($exampleMatch in $exampleMatches) + { + $exampleContent = $exampleMatch.Groups[1].Value + $exampleLines = $exampleContent -split '\r?\n' + + # Find where the description starts (first line after a blank line that follows code) + $inCodeBlock = $true + + foreach ($line in $exampleLines) + { + $trimmedLine = $line.Trim() + + if ($inCodeBlock) + { + if ([string]::IsNullOrEmpty($trimmedLine)) + { + # Blank line - marks end of code block + $inCodeBlock = $false + } + elseif ($trimmedLine -match '^#(?!region|endregion)') + { + # Found a comment in the code block (excluding #region/#endregion) + $examplesWithComments += $trimmedLine + } + } + } + } + + $examplesWithComments | Should -BeNullOrEmpty -Because ('comments within example code blocks break PlatyPS documentation generation: {0}' -f ($examplesWithComments -join ', ')) + } + } + + It 'Should not have blank lines within multi-line example code blocks for ' { + <# + PlatyPS expects .EXAMPLE blocks to have: code → blank line → description. + Blank lines within the code portion (before the description separator) cause + the documentation generator to incorrectly interpret the code block structure. + #> + if ($scriptFileRawContent -match '(?s)<#(.*?)#>') + { + $helpBlock = $Matches[1] + + # Find all .EXAMPLE blocks + $exampleMatches = [regex]::Matches($helpBlock, '(?s)\.EXAMPLE\s*\r?\n(.*?)(?=\r?\n\s*\.(?:EXAMPLE|PARAMETER|SYNOPSIS|DESCRIPTION|INPUTS|OUTPUTS|NOTES|LINK|COMPONENT|ROLE|FUNCTIONALITY)|$)') + + $examplesWithBlankLines = @() + + foreach ($exampleMatch in $exampleMatches) + { + $exampleContent = $exampleMatch.Groups[1].Value + $exampleLines = $exampleContent -split '\r?\n' + $inCodeBlock = $true + $lineNum = 0 + + foreach ($line in $exampleLines) + { + $lineNum++ + $trimmedLine = $line.Trim() + + if ($inCodeBlock) + { + if ([string]::IsNullOrEmpty($trimmedLine)) + { + # Check if next non-empty line looks like code (starts with $, command, [, or @{) + $remainingLines = $exampleLines[$lineNum..($exampleLines.Length - 1)] + $nextNonEmpty = $remainingLines | Where-Object -FilterScript { $_.Trim() } | Select-Object -First 1 + + if ($nextNonEmpty -and ($nextNonEmpty.Trim() -match '^\$|^[A-Z][a-z]+-|^\[|^@\{')) + { + $examplesWithBlankLines += "blank line at position $lineNum followed by: $($nextNonEmpty.Trim().Substring(0, [Math]::Min(30, $nextNonEmpty.Trim().Length)))..." + } + + $inCodeBlock = $false + } + } + } + } + + $examplesWithBlankLines | Should -BeNullOrEmpty -Because ('blank lines within example code blocks break PlatyPS documentation generation: {0}' -f ($examplesWithBlankLines -join '; ')) + } + } } } diff --git a/tests/Unit/DSC_SqlRS.Tests.ps1 b/tests/Unit/DSC_SqlRS.Tests.ps1 index f5305c9ca0..f17c9bc30b 100644 --- a/tests/Unit/DSC_SqlRS.Tests.ps1 +++ b/tests/Unit/DSC_SqlRS.Tests.ps1 @@ -117,7 +117,8 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force | - Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value 'NT SERVICE\SQLServerReportingServices' -PassThru -Force ), ( # Array is a regression test for issue #819. @@ -449,18 +450,6 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } -PassThru -Force } - $mockInvokeRsCimMethod_GenerateDatabaseCreationScript = { - return @{ - Script = 'select * from something' - } - } - - $mockInvokeRsCimMethod_GenerateDatabaseRightsScript = { - return @{ - Script = 'select * from something' - } - } - $mockGetCimInstance_ConfigurationSetting_NamedInstance = { return @( ( @@ -473,7 +462,8 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force | - Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value 'NT SERVICE\SQLServerReportingServices' -PassThru -Force ), ( # Array is a regression test for issue #819. @@ -495,7 +485,8 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value '' -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value '' -PassThru -Force | Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force | - Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value 'NT SERVICE\SQLServerReportingServices' -PassThru -Force } $mockGetCimInstance_ConfigurationSetting_ParameterFilter = { @@ -531,13 +522,13 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Mock -CommandName Remove-SqlDscRSUrlReservation Mock -CommandName Set-SqlDscRSUrlReservation Mock -CommandName Invoke-RsCimMethod - Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_GenerateDatabaseCreationScript -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' + Mock -CommandName Request-SqlDscRSDatabaseScript -MockWith { + return 'select * from something' } - - Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_GenerateDatabaseRightsScript -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' + Mock -CommandName Request-SqlDscRSDatabaseRightsScript -MockWith { + return 'select * from something' } + Mock -CommandName Set-SqlDscRSDatabaseConnection <# This is mocked here so that no calls are made to it directly, @@ -629,17 +620,11 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $MethodName -eq 'InitializeReportServer' } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscRSDatabaseConnection -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseRightsScript -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseScript -Exactly -Times 1 -Scope It Should -Invoke -CommandName Set-SqlDscRSVirtualDirectory -ParameterFilter { $Application -eq $mockReportServerApplicationName @@ -881,17 +866,11 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $MethodName -eq 'InitializeReportServer' } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Set-SqlDscRSDatabaseConnection -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseRightsScript -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseScript -Exactly -Times 0 -Scope It Should -Invoke -CommandName Set-SqlDscRSVirtualDirectory -ParameterFilter { $Application -eq $mockReportServerApplicationName @@ -985,17 +964,11 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $MethodName -eq 'InitializeReportServer' } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Set-SqlDscRSDatabaseConnection -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseRightsScript -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseScript -Exactly -Times 0 -Scope It Should -Invoke -CommandName Set-SqlDscRSVirtualDirectory -ParameterFilter { $Application -eq $mockReportServerApplicationName @@ -1073,17 +1046,11 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $MethodName -eq 'InitializeReportServer' } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscRSDatabaseConnection -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseRightsScript -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseScript -Exactly -Times 1 -Scope It Should -Invoke -CommandName Set-SqlDscRSVirtualDirectory -ParameterFilter { $Application -eq $mockReportServerApplicationName @@ -1138,7 +1105,8 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $script:mockDynamicSecureConnectionLevel -PassThru -Force | - Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockReportingServicesServiceName -PassThru -Force | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value 'NT SERVICE\SQLServerReportingServices' -PassThru ) } @@ -1189,17 +1157,11 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $MethodName -eq 'InitializeReportServer' } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Set-SqlDscRSDatabaseConnection -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseRightsScript -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Request-SqlDscRSDatabaseScript -Exactly -Times 1 -Scope It Should -Invoke -CommandName Set-SqlDscRSVirtualDirectory -ParameterFilter { $Application -eq $mockReportServerApplicationName diff --git a/tests/Unit/Private/Get-HResultMessage.Tests.ps1 b/tests/Unit/Private/Get-HResultMessage.Tests.ps1 new file mode 100644 index 0000000000..66238d4985 --- /dev/null +++ b/tests/Unit/Private/Get-HResultMessage.Tests.ps1 @@ -0,0 +1,125 @@ +[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' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName + + $env:SqlServerDscCI = $true +} + +AfterAll { + $env:SqlServerDscCI = $null + + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') +} + +Describe 'Get-HResultMessage' -Tag 'Private' { + Context 'When translating known HRESULT codes' { + BeforeDiscovery { + $testCases = @( + @{ + HResult = -2147024891 + ExpectedPattern = '*Access is denied*' + Description = 'E_ACCESSDENIED' + } + @{ + HResult = -2147023181 + ExpectedPattern = '*logon type*' + Description = 'ERROR_LOGON_TYPE_NOT_GRANTED' + } + @{ + HResult = -2147467259 + ExpectedPattern = '*unspecified failure*' + Description = 'E_FAIL' + } + @{ + HResult = -2147024809 + ExpectedPattern = '*arguments are not valid*' + Description = 'E_INVALIDARG' + } + @{ + HResult = -2147024882 + ExpectedPattern = '*out of memory*' + Description = 'E_OUTOFMEMORY' + } + @{ + HResult = -2147417848 + ExpectedPattern = '*disconnected*' + Description = 'RPC_E_DISCONNECTED' + } + @{ + HResult = -2147023174 + ExpectedPattern = '*RPC server is unavailable*' + Description = 'RPC_S_SERVER_UNAVAILABLE' + } + @{ + HResult = -2147023834 + ExpectedPattern = '*service has not been started*' + Description = 'ERROR_SERVICE_NOT_ACTIVE' + } + ) + } + + It 'Should return a descriptive message for (HRESULT: )' -ForEach $testCases { + InModuleScope -Parameters $_ -ScriptBlock { + $result = Get-HResultMessage -HResult $HResult + + $result | Should -BeLike $ExpectedPattern + } + } + } + + Context 'When translating an unknown HRESULT code' { + It 'Should return a generic message with the hexadecimal code' { + InModuleScope -ScriptBlock { + $result = Get-HResultMessage -HResult -2147483648 + + $result | Should -BeLike '*Unknown HRESULT*' + $result | Should -BeLike '*0x80000000*' + } + } + } + + Context 'When translating a positive HRESULT code' { + It 'Should return a generic message for positive values' { + InModuleScope -ScriptBlock { + # Positive values are typically success codes but if they don't map, return unknown + $result = Get-HResultMessage -HResult 12345 + + $result | Should -BeLike '*Unknown HRESULT*' + } + } + } +} diff --git a/tests/Unit/Private/Invoke-RsCimMethod.Tests.ps1 b/tests/Unit/Private/Invoke-RsCimMethod.Tests.ps1 index bc9c9d0bf9..90f15df7a1 100644 --- a/tests/Unit/Private/Invoke-RsCimMethod.Tests.ps1 +++ b/tests/Unit/Private/Invoke-RsCimMethod.Tests.ps1 @@ -199,4 +199,77 @@ Describe 'Invoke-RsCimMethod' -Tag 'Private' { } } } + + Context 'When CIM method fails with empty ExtendedErrors but has Error property' { + BeforeAll { + Mock -CommandName Invoke-CimMethod -MockWith { + $result = [PSCustomObject] @{ + HRESULT = 3 + Error = 'Fallback error message' + } + $result | Add-Member -MemberType NoteProperty -Name 'ExtendedErrors' -Value @() + return $result + } + } + + It 'Should fall back to Error property message' { + InModuleScope -ScriptBlock { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + { Invoke-RsCimMethod -CimInstance $mockCimInstance -MethodName 'TestMethod' } | + Should -Throw -ExpectedMessage '*TestMethod*Fallback error message*HRESULT:3*' + } + } + } + + Context 'When CIM method fails with no error details available' { + BeforeAll { + Mock -CommandName Invoke-CimMethod -MockWith { + $result = [PSCustomObject] @{ + HRESULT = 4 + Error = '' + } + $result | Add-Member -MemberType NoteProperty -Name 'ExtendedErrors' -Value @() + return $result + } + } + + It 'Should use fallback message when neither ExtendedErrors nor Error have content' { + InModuleScope -ScriptBlock { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + { Invoke-RsCimMethod -CimInstance $mockCimInstance -MethodName 'TestMethod' } | + Should -Throw -ExpectedMessage '*TestMethod*No error details were returned*Unknown HRESULT*HRESULT:4*' + } + } + } + + Context 'When CIM method fails with a known HRESULT code' { + BeforeAll { + Mock -CommandName Invoke-CimMethod -MockWith { + $result = [PSCustomObject] @{ + # ERROR_LOGON_TYPE_NOT_GRANTED (0x80070533) + HRESULT = -2147023181 + Error = '' + } + $result | Add-Member -MemberType NoteProperty -Name 'ExtendedErrors' -Value @() + return $result + } + } + + It 'Should include the translated HRESULT message in the error' { + InModuleScope -ScriptBlock { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + { Invoke-RsCimMethod -CimInstance $mockCimInstance -MethodName 'TestMethod' } | + Should -Throw -ExpectedMessage '*logon type*HRESULT:-2147023181*' + } + } + } } diff --git a/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 b/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 new file mode 100644 index 0000000000..8b4ff4c892 --- /dev/null +++ b/tests/Unit/Public/Request-SqlDscRSDatabaseRightsScript.Tests.ps1 @@ -0,0 +1,177 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Request-SqlDscRSDatabaseRightsScript' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] [-DatabaseName] [-UserName] [-IsRemote] [-UseSqlAuthentication] []' + } + ) { + $result = (Get-Command -Name 'Request-SqlDscRSDatabaseRightsScript').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 generating database rights script with default parameters' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'GRANT SELECT ON [ReportServer] TO [NT SERVICE\SQLServerReportingServices]' + } + } + } + + It 'Should generate script without errors' { + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'NT SERVICE\SQLServerReportingServices' } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' -and + $Arguments.DatabaseName -eq 'ReportServer' -and + $Arguments.UserName -eq 'NT SERVICE\SQLServerReportingServices' -and + $Arguments.IsRemote -eq $false -and + $Arguments.IsWindowsUser -eq $true + } -Exactly -Times 1 + } + + It 'Should return the script as a string' { + $result = $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'NT SERVICE\SQLServerReportingServices' + + $result | Should -Be 'GRANT SELECT ON [ReportServer] TO [NT SERVICE\SQLServerReportingServices]' + } + } + + Context 'When generating database rights script for remote database' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'GRANT SELECT ON [ReportServer] TO [DOMAIN\SQLRSUser]' + } + } + } + + It 'Should set IsRemote to true' { + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'DOMAIN\SQLRSUser' -IsRemote } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.IsRemote -eq $true + } -Exactly -Times 1 + } + } + + Context 'When generating database rights script for SQL Server authentication user' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'GRANT SELECT ON [ReportServer] TO [sqluser]' + } + } + } + + It 'Should set IsWindowsUser to false when UseSqlAuthentication is specified' { + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'sqluser' -UseSqlAuthentication } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.IsWindowsUser -eq $false + } -Exactly -Times 1 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'GRANT SELECT ON [ReportServer] TO [NT SERVICE\SQLServerReportingServices]' + } + } + } + + It 'Should generate script' { + { Request-SqlDscRSDatabaseRightsScript -Configuration $mockCimInstance -DatabaseName 'ReportServer' -UserName 'NT SERVICE\SQLServerReportingServices' } | Should -Not -Throw + + 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 GenerateDatabaseRightsScript() failed with an error.' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Request-SqlDscRSDatabaseRightsScript -DatabaseName 'ReportServer' -UserName 'NT SERVICE\SQLServerReportingServices' } | Should -Throw -ErrorId 'RSRDBRS0001,Request-SqlDscRSDatabaseRightsScript' + } + } +} diff --git a/tests/Unit/Public/Request-SqlDscRSDatabaseScript.Tests.ps1 b/tests/Unit/Public/Request-SqlDscRSDatabaseScript.Tests.ps1 new file mode 100644 index 0000000000..744607489b --- /dev/null +++ b/tests/Unit/Public/Request-SqlDscRSDatabaseScript.Tests.ps1 @@ -0,0 +1,264 @@ +[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' + + <# + Stub function for Get-Service when running on non-Windows platforms. + This allows the mock to work correctly on macOS and Linux. + #> + if (-not ($IsWindows -or $PSVersionTable.PSVersion.Major -le 5)) + { + InModuleScope -ModuleName $script:moduleName -ScriptBlock { + function script:Get-Service + { + param + ( + [Parameter()] + [System.String] + $Name, + + [Parameter()] + [System.String] + $ErrorAction + ) + } + } + } + + $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 'Request-SqlDscRSDatabaseScript' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] [-DatabaseName] [[-Lcid] ] []' + } + ) { + $result = (Get-Command -Name 'Request-SqlDscRSDatabaseScript').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 generating database creation script with default parameters' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'CREATE DATABASE [ReportServer]' + } + } + + Mock -CommandName Get-OperatingSystem -MockWith { + return [PSCustomObject] @{ + OSLanguage = 1033 + } + } + + Mock -CommandName Get-Service -MockWith { + return [PSCustomObject] @{ + Name = 'SQLServerReportingServices' + Status = 'Running' + } + } + } + + It 'Should generate script without errors' { + { $mockCimInstance | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' -and + $Arguments.DatabaseName -eq 'ReportServer' -and + $Arguments.IsSharePointMode -eq $false -and + $Arguments.Lcid -eq 1033 + } -Exactly -Times 1 + } + + It 'Should return the script as a string' { + $result = $mockCimInstance | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' + + $result | Should -Be 'CREATE DATABASE [ReportServer]' + } + } + + Context 'When generating database creation script with explicit Lcid' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'CREATE DATABASE [ReportServer]' + } + } + + Mock -CommandName Get-Service -MockWith { + return [PSCustomObject] @{ + Name = 'SQLServerReportingServices' + Status = 'Running' + } + } + } + + It 'Should use the specified Lcid' { + { $mockCimInstance | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' -Lcid 1053 } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.Lcid -eq 1053 + } -Exactly -Times 1 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + return [PSCustomObject] @{ + Script = 'CREATE DATABASE [ReportServer]' + } + } + + Mock -CommandName Get-OperatingSystem -MockWith { + return [PSCustomObject] @{ + OSLanguage = 1033 + } + } + + Mock -CommandName Get-Service -MockWith { + return [PSCustomObject] @{ + Name = 'SQLServerReportingServices' + Status = 'Running' + } + } + } + + It 'Should generate script' { + { Request-SqlDscRSDatabaseScript -Configuration $mockCimInstance -DatabaseName 'ReportServer' } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method GenerateDatabaseCreationScript() failed with an error.' + } + + Mock -CommandName Get-OperatingSystem -MockWith { + return [PSCustomObject] @{ + OSLanguage = 1033 + } + } + + Mock -CommandName Get-Service -MockWith { + return [PSCustomObject] @{ + Name = 'SQLServerReportingServices' + Status = 'Running' + } + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' } | Should -Throw -ErrorId 'RSRDBS0001,Request-SqlDscRSDatabaseScript' + } + } + + Context 'When the Reporting Services service is not running' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Get-Service -MockWith { + return [PSCustomObject] @{ + Name = 'SQLServerReportingServices' + Status = 'Stopped' + } + } + } + + It 'Should throw a terminating error with the correct error ID' { + { $mockCimInstance | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' } | Should -Throw -ErrorId 'RSRDBS0002,Request-SqlDscRSDatabaseScript' + } + } + + Context 'When the Reporting Services service does not exist' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + ServiceName = 'SQLServerReportingServices' + } + + Mock -CommandName Get-Service -MockWith { + return $null + } + } + + It 'Should throw a terminating error with the correct error ID' { + { $mockCimInstance | Request-SqlDscRSDatabaseScript -DatabaseName 'ReportServer' } | Should -Throw -ErrorId 'RSRDBS0002,Request-SqlDscRSDatabaseScript' + } + } +} diff --git a/tests/Unit/Public/Set-SqlDscRSDatabaseConnection.Tests.ps1 b/tests/Unit/Public/Set-SqlDscRSDatabaseConnection.Tests.ps1 new file mode 100644 index 0000000000..2f247db58b --- /dev/null +++ b/tests/Unit/Public/Set-SqlDscRSDatabaseConnection.Tests.ps1 @@ -0,0 +1,317 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies have been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies have not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks noop" first.' + } +} + +BeforeAll { + $script:moduleName = 'SqlServerDsc' + + $env:SqlServerDscCI = $true + + Import-Module -Name $script:moduleName -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Item -Path 'env:SqlServerDscCI' +} + +Describe 'Set-SqlDscRSDatabaseConnection' { + Context 'When validating parameter sets' { + It 'Should have the correct parameters in parameter set ' -ForEach @( + @{ + ExpectedParameterSetName = '__AllParameterSets' + ExpectedParameters = '[-Configuration] [-ServerName] [[-InstanceName] ] [-DatabaseName] [[-Type] ] [[-Credential] ] [-PassThru] [-Force] [-WhatIf] [-Confirm] []' + } + ) { + $result = (Get-Command -Name 'Set-SqlDscRSDatabaseConnection').ParameterSets | + Where-Object -FilterScript { $_.Name -eq $ExpectedParameterSetName } | + Select-Object -Property @( + @{ Name = 'ParameterSetName'; Expression = { $_.Name } }, + @{ Name = 'ParameterListAsString'; Expression = { $_.ToString() } } + ) + + $result.ParameterSetName | Should -Be $ExpectedParameterSetName + $result.ParameterListAsString | Should -Be $ExpectedParameters + } + } + + Context 'When setting database connection with ServiceAccount credentials type' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set database connection without errors' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' -and + $Arguments.Server -eq 'localhost' -and + $Arguments.DatabaseName -eq 'ReportServer' -and + $Arguments.Username -eq '' -and + $Arguments.Password -eq '' -and + $Arguments.CredentialsType -eq 2 + } -Exactly -Times 1 + } + + It 'Should not return anything by default' { + $result = $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Confirm:$false + + $result | Should -BeNullOrEmpty + } + } + + Context 'When setting database connection with PassThru' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should return the configuration CIM instance' { + $result = $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -PassThru -Confirm:$false + + $result | Should -Not -BeNullOrEmpty + $result.InstanceName | Should -Be 'SSRS' + } + } + + Context 'When setting database connection with Force' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set database connection without confirmation' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Force } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When setting database connection with Windows credentials type' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'DOMAIN\User', + (ConvertTo-SecureString -String 'Password123' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should use the Windows credentials type (0)' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Type 'Windows' -Credential $mockCredential -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.CredentialsType -eq 0 -and + $Arguments.Username -eq 'DOMAIN\User' -and + $Arguments.Password -eq 'Password123' + } -Exactly -Times 1 + } + } + + Context 'When setting database connection with SqlServer credentials type' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + $mockCredential = [System.Management.Automation.PSCredential]::new( + 'sa', + (ConvertTo-SecureString -String 'SqlPassword123' -AsPlainText -Force) + ) + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should use the SqlServer credentials type (1)' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Type 'SqlServer' -Credential $mockCredential -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.CredentialsType -eq 1 -and + $Arguments.Username -eq 'sa' -and + $Arguments.Password -eq 'SqlPassword123' + } -Exactly -Times 1 + } + } + + Context 'When setting database connection with ServiceAccount credentials type explicitly' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should use the ServiceAccount credentials type (2)' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Type 'ServiceAccount' -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.CredentialsType -eq 2 -and + $Arguments.Username -eq '' -and + $Arguments.Password -eq '' + } -Exactly -Times 1 + } + } + + Context 'When Windows credentials type is specified without Credential parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Type 'Windows' -Confirm:$false } | Should -Throw -ErrorId 'SSRSDC0002,Set-SqlDscRSDatabaseConnection' + } + } + + Context 'When SqlServer credentials type is specified without Credential parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Type 'SqlServer' -Confirm:$false } | Should -Throw -ErrorId 'SSRSDC0002,Set-SqlDscRSDatabaseConnection' + } + } + + Context 'When CIM method fails' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw 'Method SetDatabaseConnection() failed with an error. Error: Access denied (HRESULT:-2147024891)' + } + } + + It 'Should throw a terminating error' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -Confirm:$false } | Should -Throw -ErrorId 'SSRSDC0001,Set-SqlDscRSDatabaseConnection' + } + } + + Context 'When using WhatIf' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should not call Invoke-RsCimMethod' { + $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer' -WhatIf + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 0 + } + } + + Context 'When passing configuration as parameter' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should set database connection' { + { Set-SqlDscRSDatabaseConnection -Configuration $mockCimInstance -ServerName 'localhost' -DatabaseName 'ReportServer' -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times 1 + } + } + + Context 'When using named instance database server' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should pass the correct server name format when InstanceName is specified' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'SqlServer01' -InstanceName 'MSSQLSERVER' -DatabaseName 'ReportServer' -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.Server -eq 'SqlServer01\MSSQLSERVER' + } -Exactly -Times 1 + } + + It 'Should pass only the server name when InstanceName is not specified' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'SqlServer01' -DatabaseName 'ReportServer' -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.Server -eq 'SqlServer01' + } -Exactly -Times 1 + } + } + + Context 'When using custom database name' { + BeforeAll { + $mockCimInstance = [PSCustomObject] @{ + InstanceName = 'SSRS' + } + + Mock -CommandName Invoke-RsCimMethod + } + + It 'Should pass the correct database name' { + { $mockCimInstance | Set-SqlDscRSDatabaseConnection -ServerName 'localhost' -DatabaseName 'ReportServer$SSRS' -Confirm:$false } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $Arguments.DatabaseName -eq 'ReportServer$SSRS' + } -Exactly -Times 1 + } + } +}