diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 342c43c855..f123e70475 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,7 +1,12 @@ -# AI Instructions for SqlServerDsc +# AI Instructions -This file provides AI agent guidance for the SqlServerDsc project. Each -instruction file below targets specific file glob patterns and use cases. +This file provides AI agent guidance for the project. Each instruction file below +targets specific file glob patterns and use cases. + +## Build & Test Workflow +- Run in PowerShell, from repository root +- Build before running tests: `.\build.ps1 -Tasks build` +- Always run tests in new PowerShell session: `Invoke-Pester -Path @({test paths}) -Output Detailed` ## Instructions Overview diff --git a/.github/instructions/SqlServerDsc-guidelines.instructions.md b/.github/instructions/SqlServerDsc-guidelines.instructions.md index 5202dbd54a..cc8ec878ae 100644 --- a/.github/instructions/SqlServerDsc-guidelines.instructions.md +++ b/.github/instructions/SqlServerDsc-guidelines.instructions.md @@ -11,7 +11,9 @@ applyTo: "**" ## Resources - Database Engine resources: inherit `SqlResourceBase` -- `SqlResourceBase` provides: `InstanceName`, `ServerName`, `Credential`, `Reasons`, `GetServerObject()` + - Add `InstanceName`, `ServerName`, and `Credential` to `$this.ExcludeDscProperties` + - `SqlResourceBase` provides: `InstanceName`, `ServerName`, `Credential`, `Reasons`, `GetServerObject()` + - Constructor: `MyResourceName() : base () { }` (no $PSScriptRoot parameter) ## SQL Server Interaction - Always prefer SMO over T-SQL @@ -34,6 +36,7 @@ applyTo: "**" - Choose the appropriate group number based on the required dependencies ## Unit tests -- When unit test uses SMO types, ensure they are properly stubbed in SMO.cs -- Load stub types from SMO.cs in unit test files, e.g. `Add-Type -Path "$PSScriptRoot/../Stubs/SMO.cs"` -- After changing SMO stub types, run tests in a new PowerShell session for changes to take effect. +- When unit test tests classes or commands that contain SMO types, e.g. `[Microsoft.SqlServer.Management.Smo.*]` + - Ensure they are properly stubbed in SMO.cs + - Load SMO stub types from SMO.cs in unit test files, e.g. `Add-Type -Path "$PSScriptRoot/../Stubs/SMO.cs"` + - After changing SMO stub types, run tests in a new PowerShell session for changes to take effect. diff --git a/.github/instructions/dsc-community-style-guidelines-changelog.instructions.md b/.github/instructions/dsc-community-style-guidelines-changelog.instructions.md index f2075f00b6..5041db8fa0 100644 --- a/.github/instructions/dsc-community-style-guidelines-changelog.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-changelog.instructions.md @@ -8,6 +8,6 @@ applyTo: "CHANGELOG.md" - Always update the Unreleased section in CHANGELOG.md - Use Keep a Changelog format - Describe notable changes briefly, ≤2 items per change type -- Reference issues using format [#](https://github.com///issues/) +- Reference issues using format [issue #](https://github.com///issues/) - No empty lines between list items in same section - Do not add item if there are already an existing item for the same change diff --git a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md index e018d98166..abd5a3766a 100644 --- a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md @@ -12,13 +12,14 @@ applyTo: "source/[cC]lasses/**/*.ps1" - Decoration: `[DscResource(RunAsCredential = 'Optional')]` (replace with `'Mandatory'` if required) - Inheritance: Must inherit `ResourceBase` (part of module DscResource.Base) - `$this.localizedData` hashtable auto-populated by `ResourceBase` from localization file +- value-type properties: Use `[Nullable[{FullTypeName}]]` (e.g., `[Nullable[System.Int32]]`) ## Required constructor ```powershell MyResourceName () : base ($PSScriptRoot) { - # Property names where state cannot be enforced, e.g Ensure + # Property names where state cannot be enforced, e.g. IsSingleInstance, Force $this.ExcludeDscProperties = @() } ``` @@ -78,3 +79,19 @@ hidden [void] NormalizeProperties([System.Collections.Hashtable] $properties) # Normalize user-provided properties, $properties contains user assigned values } ``` + +## Required comment-based help + +Add to .DESCRIPTION section: +- `## Requirements`: List minimum requirements +- `## Known issues`: Critical issues + pattern: `All issues are not listed here, see [all open issues](https://github.com/{owner}/{repo}/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+{ResourceName}).` + +## Error Handling +- Use `try/catch` blocks to handle exceptions +- Do not use `throw` for terminating errors, use `New-*Exception` commands: + - [`New‑InvalidDataException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidDataException) + - [`New-ArgumentException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ArgumentException) + - [`New-InvalidOperationException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidOperationException) + - [`New-ObjectNotFoundException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91ObjectNotFoundException) + - [`New-InvalidResultException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91InvalidResultException) + - [`New-NotImplementedException`](https://github.com/dsccommunity/DscResource.Common/wiki/New%E2%80%91NotImplementedException) diff --git a/.github/instructions/dsc-community-style-guidelines-integration-tests.instructions.md b/.github/instructions/dsc-community-style-guidelines-integration-tests.instructions.md index affb1a3804..8c303aee42 100644 --- a/.github/instructions/dsc-community-style-guidelines-integration-tests.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-integration-tests.instructions.md @@ -45,8 +45,8 @@ BeforeDiscovery { } BeforeAll { - $script:dscModuleName = 'SqlServerDsc' + $script:moduleName = '{MyModuleName}' - Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' } ``` diff --git a/.github/instructions/dsc-community-style-guidelines-localization.instructions.md b/.github/instructions/dsc-community-style-guidelines-localization.instructions.md index ad9bc1687b..0fb1a32ebf 100644 --- a/.github/instructions/dsc-community-style-guidelines-localization.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-localization.instructions.md @@ -11,11 +11,11 @@ applyTo: "source/**/*.ps1" - Assume `$script:localizedData` is available ## String Files -- Commands/functions: `source/en-US/SqlServerDsc.strings.psd1` +- Commands/functions: `source/en-US/{MyModuleName}.strings.psd1` - Class resources: `source/en-US/{ResourceClassName}.strings.psd1` ## Key Naming Patterns -- Format: `Verb_FunctionName_Action` (underscore separators), e.g. `Get_SqlDscDatabase_ConnectingToDatabase` +- Format: `Verb_FunctionName_Action` (underscore separators), e.g. `Get_Database_ConnectingToDatabase` ## String Format ```powershell diff --git a/.github/instructions/dsc-community-style-guidelines-pester.instructions.md b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md index fc9c2fe2fe..42c3e22617 100644 --- a/.github/instructions/dsc-community-style-guidelines-pester.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md @@ -9,15 +9,21 @@ applyTo: "**/*.[Tt]ests.ps1" - All public commands, private functions and classes must have unit tests - All public commands and class-based resources must have integration tests - Use Pester v5 syntax only -- One `Describe` block per file matching the tested entity name - Test code only inside `Describe` blocks - Assertions only in `It` blocks - Never test verbose messages, debug messages or parameter binding behavior - Pass all mandatory parameters to avoid prompts +## Naming +- One `Describe` block per file matching the tested entity name +- `Context` descriptions start with 'When' +- `It` descriptions start with 'Should', must not contain 'when' +- Mock variables prefix: 'mock' + ## Structure & Scope - Public commands: Never use `InModuleScope` (unless retrieving localized strings) - Private functions/class resources: Always use `InModuleScope` +- Each class method = separate `Context` block - Each scenario = separate `Context` block - Use nested `Context` blocks for complex scenarios - Mocking in `BeforeAll` (`BeforeEach` only when required) @@ -25,15 +31,14 @@ applyTo: "**/*.[Tt]ests.ps1" ## Syntax Rules - PascalCase: `Describe`, `Context`, `It`, `Should`, `BeforeAll`, `BeforeEach`, `AfterAll`, `AfterEach` -- `Context` descriptions start with 'When' -- `It` descriptions start with 'Should', must not contain 'when' -- Mock variables prefix: 'mock' - Prefer `-BeTrue`/`-BeFalse` over `-Be $true`/`-Be $false` +- Never use `Assert-MockCalled`, use `Should -Invoke` instead - No `Should -Not -Throw` - invoke commands directly - Never add an empty `-MockWith` block - Omit `-MockWith` when returning `$null` - Set `$PSDefaultParameterValues` for `Mock:ModuleName`, `Should:ModuleName`, `InModuleScope:ModuleName` - Omit `-ModuleName` parameter on Pester commands +- Never use `Mock` inside `InModuleScope`-block ## File Organization - Class resources: `tests/Unit/Classes/{Name}.Tests.ps1` diff --git a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md index 0ff1b6b903..1b70f9451f 100644 --- a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md @@ -49,7 +49,7 @@ applyTo: "**/*.ps?(m|d)1" ### Hashtables - Empty: `@{}` -- Multi-line: each property on separate line with proper indentation +- Each property on separate line with proper indentation - Properties: Use PascalCase ### Comments @@ -79,14 +79,18 @@ applyTo: "**/*.ps?(m|d)1" - For state-changing functions, use `SupportsShouldProcess` - Place ShouldProcess check immediately before each state-change - `$PSCmdlet.ShouldProcess` must use required pattern -- Use `$PSCmdlet.ThrowTerminatingError()` for terminating errors, use relevant error category -- Use `Write-Error` for non-terminating errors, use relevant error category -- Use `Write-Warning` for warnings -- Use `Write-Debug` for debugging information -- Use `Write-Verbose` for actionable information -- Use `Write-Information` for informational messages. - Never use backtick as line continuation in production code. +## Output streams + +- Never output sensitive data/secrets +- Use `Write-Debug` for: Internal diagnostics; Variable values/traces; Developer-focused details +- Use `Write-Verbose` for: High-level execution flow only; User-actionable information +- Use `Write-Information` for: User-facing status updates; Important operational messages; Non-error state changes +- Use `Write-Warning` for: Non-fatal issues requiring attention; Deprecated functionality usage; Configuration problems that don't block execution +- Use `$PSCmdlet.ThrowTerminatingError()` for terminating errors (except for classes), use relevant error category +- Use `Write-Error` for non-terminating errors, use relevant error category + ## ShouldProcess Required Pattern - Ensure `$descriptionMessage` explains what will happen diff --git a/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md b/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md index e798f97db6..5cebdb743a 100644 --- a/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md @@ -42,13 +42,13 @@ BeforeDiscovery { } BeforeAll { - $script:dscModuleName = 'SqlServerDsc' + $script:moduleName = '{MyModuleName}' - Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName - $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName } AfterAll { @@ -57,7 +57,7 @@ AfterAll { $PSDefaultParameterValues.Remove('Should:ModuleName') # Unload the module being tested so that it doesn't impact any other tests. - Get-Module -Name $script:dscModuleName -All | Remove-Module -Force + Get-Module -Name $script:moduleName -All | Remove-Module -Force } ``` diff --git a/.github/instructions/dsc-community-style-guidelines.instructions.md b/.github/instructions/dsc-community-style-guidelines.instructions.md index 4d66738fb4..ccc3bb3fe9 100644 --- a/.github/instructions/dsc-community-style-guidelines.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines.instructions.md @@ -11,10 +11,9 @@ applyTo: "**" - **Resource**: DSC class-based resource ## Build & Test Workflow -- Run project scripts in PowerShell from repository root -- Build after source changes: `.\build.ps1 -Tasks build` -- Test workflow: Build → `Invoke-Pester -Path @('') -Output Detailed` -- New session required after class changes +- Run in PowerShell, from repository root +- Build before running tests: `.\build.ps1 -Tasks build` +- Always run tests in new PowerShell session: `Invoke-Pester -Path @({test paths}) -Output Detailed` ## File Organization - Public commands: `source/Public/{CommandName}.ps1` diff --git a/.vscode/settings.json b/.vscode/settings.json index 804a27000c..17c15a6e1f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -89,7 +89,8 @@ "wsfc", "SOURCEBRANCH", "SOURCEBRANCHNAME", - "setvariable" + "setvariable", + "RAISERROR" ], "cSpell.ignorePaths": [ ".git" diff --git a/CHANGELOG.md b/CHANGELOG.md index 22054de4d3..f12e38fa5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added setup workflow for GitHub Copilot. - Switch the workflow to use Linux. - Attempt to unshallow the Copilot branch +- `SqlAgentAlert` + - Added new DSC resource to manage SQL Server Agent alerts. - Improved AI instructions. - Enhanced workflow with proper environment variable configuration and DSCv3 verification. - Fixed environment variable persistence by using $GITHUB_ENV instead of diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 new file mode 100644 index 0000000000..a447a8d659 --- /dev/null +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -0,0 +1,321 @@ +<# + .SYNOPSIS + The `SqlAgentAlert` DSC resource is used to create, modify, or remove + _SQL Server Agent_ alerts. + + .DESCRIPTION + The `SqlAgentAlert` DSC resource is used to create, modify, or remove + _SQL Server Agent_ alerts. + + An alert can be switched between a system-message–based alert and a severity-based + alert by specifying the corresponding parameter. The alert type will be switched + accordingly. + + The built-in parameter **PSDscRunAsCredential** can be used to run the resource + as another user. The resource will then authenticate to the _SQL Server_ + instance as that user. It is also possible to use impersonation via the + **Credential** parameter. + + ## Requirements + + * Target machine must be running Windows Server 2012 or later. + * Target machine must be running SQL Server Database Engine 2012 or later. + * Target machine must have access to the SQLPS PowerShell module or the SqlServer + PowerShell module. + + ## Known issues + + All issues are not listed here, see [here for all open issues](https://github.com/dsccommunity/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlAgentAlert). + + ### Property **Reasons** does not work with **PSDscRunAsCredential** + + When using the built-in parameter **PSDscRunAsCredential** the read-only + property **Reasons** will return empty values for the properties **Code** + and **Phrase**. The built-in property **PSDscRunAsCredential** does not work + together with class-based resources that use advanced types, such as the + **Reasons** parameter. + + ### Using **Credential** property + + SQL Authentication and Group Managed Service Accounts are not supported as + impersonation credentials. Currently, only Windows Integrated Security is + supported. + + For Windows Authentication the username must either be provided with the User + Principal Name (UPN), e.g., `username@domain.local`, or, if using a non‑domain + account (for example, a local Windows Server account), the username must be + provided without the NetBIOS name, e.g., `username`. Using the NetBIOS name, + for example `DOMAIN\username`, will not work. + + See more information in [Credential Overview](https://github.com/dsccommunity/SqlServerDsc/wiki/CredentialOverview). + + .PARAMETER Name + The name of the _SQL Server Agent_ alert. + + .PARAMETER Ensure + Specifies if the _SQL Server Agent_ alert should be present or absent. + Default value is `'Present'`. + + .PARAMETER Severity + The severity of the _SQL Server Agent_ alert. Valid range is 1 to 25. + Cannot be used together with **MessageId**. + + .PARAMETER MessageId + The message id of the _SQL Server Agent_ alert. Valid range is 1 to 2147483647. + Cannot be used together with **Severity**. + + .EXAMPLE + Invoke-DscResource -ModuleName SqlServerDsc -Name SqlAgentAlert -Method Get -Property @{ + InstanceName = 'MSSQLSERVER' + Name = 'Alert1' + } + + This example shows how to get the current state of the _SQL Server Agent_ + alert named **Alert1**. + + .EXAMPLE + Invoke-DscResource -ModuleName SqlServerDsc -Name SqlAgentAlert -Method Test -Property @{ + InstanceName = 'MSSQLSERVER' + Name = 'Alert1' + Ensure = 'Present' + Severity = 16 + } + + This example shows how to test if the _SQL Server Agent_ alert named + **Alert1** is in the desired state. + + .EXAMPLE + Invoke-DscResource -ModuleName SqlServerDsc -Name SqlAgentAlert -Method Set -Property @{ + InstanceName = 'MSSQLSERVER' + Name = 'Alert1' + Ensure = 'Present' + Severity = 16 + } + + This example shows how to set the desired state for the _SQL Server Agent_ + alert named **Alert1** with severity level 16. + + .EXAMPLE + Invoke-DscResource -ModuleName SqlServerDsc -Name SqlAgentAlert -Method Set -Property @{ + InstanceName = 'MSSQLSERVER' + Name = 'Alert1' + Ensure = 'Present' + MessageId = 50001 + } + + This example shows how to set the desired state for the _SQL Server Agent_ + alert named **Alert1** with message ID 50001. + + .EXAMPLE + Invoke-DscResource -ModuleName SqlServerDsc -Name SqlAgentAlert -Method Set -Property @{ + InstanceName = 'MSSQLSERVER' + Name = 'Alert1' + Ensure = 'Absent' + } + + This example shows how to remove the _SQL Server Agent_ alert named + **Alert1**. +#> +[DscResource(RunAsCredential = 'Optional')] +class SqlAgentAlert : SqlResourceBase +{ + [DscProperty(Key)] + [System.String] + $Name + + [DscProperty()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present' + + [DscProperty()] + [ValidateRange(1, 25)] + [Nullable[System.Int32]] + $Severity + + [DscProperty()] + [ValidateRange(1, 2147483647)] + [Nullable[System.Int32]] + $MessageId + + SqlAgentAlert () : base () + { + # Property names that cannot be enforced + $this.ExcludeDscProperties = @( + 'InstanceName', + 'ServerName', + 'Credential' + 'Name' + ) + } + + [SqlAgentAlert] Get() + { + # Call base implementation to get current state + $currentState = ([ResourceBase] $this).Get() + + return $currentState + } + + [System.Boolean] Test() + { + # Call base implementation to test current state + $inDesiredState = ([ResourceBase] $this).Test() + + return $inDesiredState + } + + [void] Set() + { + # Call base implementation to set desired state + ([ResourceBase] $this).Set() + } + + hidden [void] AssertProperties([System.Collections.Hashtable] $properties) + { + # Validate that at least one of Severity or MessageId is specified + $assertAtLeastOneParams = @{ + BoundParameterList = $properties + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertAtLeastOneParams + + # Validate that both Severity and MessageId are not specified + $assertMutuallyExclusiveParams = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } + + Assert-BoundParameter @assertMutuallyExclusiveParams + + # When Ensure is 'Absent', Severity and MessageId must not be set + $assertAbsentParams = @{ + BoundParameterList = $properties + NotAllowedList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Absent' + } + } + + Assert-BoundParameter @assertAbsentParams + } + + hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) + { + $serverObject = $this.GetServerObject() + + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_GettingCurrentState -f $this.Name, $this.InstanceName) + + $currentState = @{ + InstanceName = $this.InstanceName + ServerName = $this.ServerName + Name = $this.Name + Ensure = 'Absent' + } + + $alertObject = $serverObject | Get-SqlDscAgentAlert -Name $this.Name -ErrorAction 'SilentlyContinue' + + if ($alertObject) + { + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_AlertExists -f $this.Name) + + $currentState.Ensure = 'Present' + + # Get the current severity and message ID + if ($alertObject.Severity -gt 0) + { + $currentState.Severity = $alertObject.Severity + } + + if ($alertObject.MessageId -gt 0) + { + $currentState.MessageId = $alertObject.MessageId + } + } + else + { + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_AlertDoesNotExist -f $this.Name) + } + + return $currentState + } + + hidden [void] Modify([System.Collections.Hashtable] $properties) + { + $serverObject = $this.GetServerObject() + + if ($this.Ensure -eq 'Present') + { + $alertObject = $serverObject | Get-SqlDscAgentAlert -Name $this.Name -ErrorAction 'SilentlyContinue' + + if ($null -eq $alertObject) + { + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_CreatingAlert -f $this.Name) + + $newAlertParameters = @{ + ServerObject = $serverObject + Name = $this.Name + ErrorAction = 'Stop' + } + + if ($properties.ContainsKey('Severity')) + { + $newAlertParameters.Severity = $properties.Severity + } + + if ($properties.ContainsKey('MessageId')) + { + $newAlertParameters.MessageId = $properties.MessageId + } + + $null = New-SqlDscAgentAlert @newAlertParameters + } + else + { + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_UpdatingAlert -f $this.Name) + + $setAlertParameters = @{ + AlertObject = $alertObject + ErrorAction = 'Stop' + } + + $needsUpdate = $false + + if ($properties.ContainsKey('Severity') -and $alertObject.Severity -ne $properties.Severity) + { + $setAlertParameters.Severity = $properties.Severity + $needsUpdate = $true + } + + if ($properties.ContainsKey('MessageId') -and $alertObject.MessageId -ne $properties.MessageId) + { + $setAlertParameters.MessageId = $properties.MessageId + $needsUpdate = $true + } + + if ($needsUpdate) + { + $null = Set-SqlDscAgentAlert @setAlertParameters + } + else + { + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_NoChangesNeeded -f $this.Name) + } + } + } + else # Ensure = 'Absent' + { + Write-Verbose -Message ($this.localizedData.SqlAgentAlert_RemovingAlert -f $this.Name) + + $null = $serverObject | Remove-SqlDscAgentAlert -Name $this.Name -Force -ErrorAction 'Stop' + } + } +} diff --git a/source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.psm1 b/source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.psm1 deleted file mode 100644 index d43d4c0057..0000000000 --- a/source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.psm1 +++ /dev/null @@ -1,372 +0,0 @@ -$script:sqlServerDscHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\SqlServerDsc.Common' -$script:resourceHelperModulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\Modules\DscResource.Common' - -Import-Module -Name $script:sqlServerDscHelperModulePath -Import-Module -Name $script:resourceHelperModulePath - -$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' - -<# - .SYNOPSIS - This function gets the SQL Agent Alert. - - .PARAMETER Name - The name of the SQL Agent Alert. - - .PARAMETER ServerName - The host name of the SQL Server to be configured. Default is the current - computer name. - - .PARAMETER InstanceName - The name of the SQL instance to be configured. -#> -function Get-TargetResource -{ - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param - ( - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Name, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $ServerName = (Get-ComputerName), - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $InstanceName - - ) - - $returnValue = @{ - Name = $null - Ensure = 'Absent' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = $null - MessageId = $null - } - - $sqlServerObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' - - if ($sqlServerObject) - { - Write-Verbose -Message ( - $script:localizedData.GetSqlAlerts - ) - # Check agent exists - $sqlAgentObject = $sqlServerObject.JobServer.Alerts | Where-Object -FilterScript { $_.Name -eq $Name } - if ($sqlAgentObject) - { - Write-Verbose -Message ( - $script:localizedData.SqlAlertPresent ` - -f $Name - ) - $returnValue['Ensure'] = 'Present' - $returnValue['Name'] = $sqlAgentObject.Name - $returnValue['Severity'] = $sqlAgentObject.Severity - $returnValue['MessageId'] = $sqlAgentObject.MessageId - } - else - { - Write-Verbose -Message ( - $script:localizedData.SqlAlertAbsent ` - -f $Name - ) - } - } - else - { - $errorMessage = $script:localizedData.ConnectServerFailed -f $ServerName, $InstanceName - New-InvalidOperationException -Message $errorMessage - } - - return $returnValue -} - -<# - .SYNOPSIS - This function sets the SQL Agent Alert. - - .PARAMETER Ensure - Specifies if the SQL Agent Alert should be present or absent. Default is Present - - .PARAMETER Name - The name of the SQL Agent Alert. - - .PARAMETER ServerName - The host name of the SQL Server to be configured. Default is the current - computer name. - - .PARAMETER InstanceName - The name of the SQL instance to be configured. - - .PARAMETER Severity - The severity of the SQL Agent Alert. - - .PARAMETER MessageId - The messageid of the SQL Agent Alert. -#> -function Set-TargetResource -{ - [CmdletBinding()] - param - ( - [Parameter()] - [ValidateSet('Present', 'Absent')] - [ValidateNotNullOrEmpty()] - [System.String] - $Ensure = 'Present', - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Name, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $ServerName = (Get-ComputerName), - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $InstanceName, - - [Parameter()] - [System.String] - $Severity, - - [Parameter()] - [System.String] - $MessageId - ) - - $sqlServerObject = Connect-SQL -ServerName $ServerName -InstanceName $InstanceName -ErrorAction 'Stop' - - if ($sqlServerObject) - { - switch ($Ensure) - { - 'Present' - { - $sqlAlertObject = $sqlServerObject.JobServer.Alerts | Where-Object -FilterScript { $_.Name -eq $Name } - if ($sqlAlertObject) - { - if ($PSBoundParameters.ContainsKey('Severity') -and $PSBoundParameters.ContainsKey('MessageId')) - { - $errorMessage = $script:localizedData.MultipleParameterError -f $Name - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - - if ($PSBoundParameters.ContainsKey('Severity')) - { - try - { - Write-Verbose -Message ( - $script:localizedData.UpdateSeverity ` - -f $Severity, $Name - ) - $sqlAlertObject.MessageId = 0 - $sqlAlertObject.Severity = $Severity - $sqlAlertObject.Alter() - } - catch - { - $errorMessage = $script:localizedData.UpdateAlertSeverityError -f $ServerName, $InstanceName, $Name, $Severity - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - } - - if ($PSBoundParameters.ContainsKey('MessageId')) - { - try - { - Write-Verbose -Message ( - $script:localizedData.UpdateMessageId ` - -f $MessageId, $Name - ) - $sqlAlertObject.Severity = 0 - $sqlAlertObject.MessageId = $MessageId - $sqlAlertObject.Alter() - } - catch - { - $errorMessage = $script:localizedData.UpdateAlertMessageIdError -f $ServerName, $InstanceName, $Name, $MessageId - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - } - } - else - { - try - { - $sqlAlertObjectToCreate = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Agent.Alert -ArgumentList $sqlServerObject.JobServer, $Name - - if ($sqlAlertObjectToCreate) - { - Write-Verbose -Message ( - $script:localizedData.AddSqlAgentAlert ` - -f $Name - ) - if ($PSBoundParameters.ContainsKey('Severity')) - { - Write-Verbose -Message ( - $script:localizedData.UpdateSeverity ` - -f $Severity, $Name - ) - $sqlAlertObjectToCreate.Severity = $Severity - } - if ($PSBoundParameters.ContainsKey('MessageId')) - { - Write-Verbose -Message ( - $script:localizedData.UpdateMessageId ` - -f $MessageId, $Name - ) - $sqlAlertObjectToCreate.MessageId = $MessageId - } - $sqlAlertObjectToCreate.Create() - } - } - catch - { - $errorMessage = $script:localizedData.CreateAlertSetError -f $Name, $ServerName, $InstanceName - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - } - } - - 'Absent' - { - try - { - $sqlAlertObjectToDrop = $sqlServerObject.JobServer.Alerts | Where-Object -FilterScript { $_.Name -eq $Name } - if ($sqlAlertObjectToDrop) - { - Write-Verbose -Message ( - $script:localizedData.DeleteSqlAgentAlert ` - -f $Name - ) - $sqlAlertObjectToDrop.Drop() - } - } - catch - { - $errorMessage = $script:localizedData.DropAlertSetError -f $Name, $ServerName, $InstanceName - New-InvalidOperationException -Message $errorMessage -ErrorRecord $_ - } - } - } - } - else - { - $errorMessage = $script:localizedData.ConnectServerFailed -f $ServerName, $InstanceName - New-InvalidOperationException -Message $errorMessage - } -} - -<# - .SYNOPSIS - This function tests the SQL Agent Alert. - - .PARAMETER Ensure - Specifies if the SQL Agent Alert should be present or absent. Default is Present - - .PARAMETER Name - The name of the SQL Agent Alert. - - .PARAMETER ServerName - The host name of the SQL Server to be configured. Default is the current - computer name. - - .PARAMETER InstanceName - The name of the SQL instance to be configured. - - .PARAMETER Severity - The severity of the SQL Agent Alert. - - .PARAMETER MessageId - The messageid of the SQL Agent Alert. -#> - -function Test-TargetResource -{ - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('SqlServerDsc.AnalyzerRules\Measure-CommandsNeededToLoadSMO', '', Justification = 'The command Connect-Sql is called when Get-TargetResource is called')] - [CmdletBinding()] - [OutputType([System.Boolean])] - param - ( - [Parameter()] - [ValidateSet('Present', 'Absent')] - [System.String] - $Ensure = 'Present', - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $Name, - - [Parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [System.String] - $InstanceName, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [System.String] - $ServerName = (Get-ComputerName), - - [Parameter()] - [System.String] - $Severity, - - [Parameter()] - [System.String] - $MessageId - ) - - $getTargetResourceParameters = @{ - ServerName = $ServerName - InstanceName = $InstanceName - Name = $Name - } - - $returnValue = $false - - $getTargetResourceResult = Get-TargetResource @getTargetResourceParameters - - Write-Verbose -Message ( - $script:localizedData.TestingConfiguration - ) - - if ($Ensure -eq 'Present') - { - $testDscParameterStateParameters = @{ - CurrentValues = $getTargetResourceResult - DesiredValues = $PSBoundParameters - ValuesToCheck = @( - 'Name' - 'Severity' - 'MessageId' - ) - TurnOffTypeChecking = $true - } - - $returnValue = Test-DscParameterState @testDscParameterStateParameters - } - else - { - if ($Ensure -eq $getTargetResourceResult.Ensure) - { - $returnValue = $true - } - } - - return $returnValue -} diff --git a/source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.schema.mof b/source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.schema.mof deleted file mode 100644 index 35630e1850..0000000000 --- a/source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.schema.mof +++ /dev/null @@ -1,10 +0,0 @@ -[ClassVersion("1.0.0.0"), FriendlyName("SqlAgentAlert")] -class DSC_SqlAgentAlert : OMI_BaseResource -{ - [Key, Description("The name of the _SQL Server Agent_ alert.")] String Name; - [Write, Description("Specifies if the _SQL Server Agent_ alert should be present or absent. Default value is `'Present'`."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; - [Write, Description("The host name of the _SQL Server_ to be configured. Default value is the current computer name.")] String ServerName; - [Key, Description("The name of the _SQL Server_ instance to be configured.")] String InstanceName; - [Write, Description("The severity of the _SQL Server Agent_ alert.")] String Severity; - [Write, Description("The message id of the _SQL Server Agent_ alert.")] String MessageId; -}; diff --git a/source/DSCResources/DSC_SqlAgentAlert/README.md b/source/DSCResources/DSC_SqlAgentAlert/README.md deleted file mode 100644 index 7123b98e5b..0000000000 --- a/source/DSCResources/DSC_SqlAgentAlert/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Description - -The `SqlAgentAlert` DSC resource is used to add/remove SQL Agent Alerts. -The resource can also update the severity or message id. - -## Requirements - -* Target machine must be running Windows Server 2012 or later. -* Target machine must be running SQL Server Database Engine 2012 or later. - -## Known issues - -All issues are not listed here, see [here for all open issues](https://github.com/dsccommunity/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlAgentAlert). diff --git a/source/DSCResources/DSC_SqlAgentAlert/en-US/DSC_SqlAgentAlert.strings.psd1 b/source/DSCResources/DSC_SqlAgentAlert/en-US/DSC_SqlAgentAlert.strings.psd1 deleted file mode 100644 index 3f9164d5aa..0000000000 --- a/source/DSCResources/DSC_SqlAgentAlert/en-US/DSC_SqlAgentAlert.strings.psd1 +++ /dev/null @@ -1,18 +0,0 @@ -# Localized resources for DSC_SqlServerAgentAlert - -ConvertFrom-StringData @' - GetSqlAlerts = Getting SQL Agent Alerts. - SqlAlertPresent = SQL Agent Alert '{0}' is present. - SqlAlertAbsent = SQL Agent Alert '{0}' is absent. - UpdateSeverity = Updating severity to '{0}' for SQL Agent Alert '{1}'. - UpdateMessageId = Updating message id to '{0}' for SQL Agent Alert '{1}'. - UpdateAlertSeverityError = Unable to update the severity for '{2}' to '{3}' on {0}\\{1}. - UpdateAlertMessageIdError = Unable to update the message id for '{2}' to '{3}' on {0}\\{1}. - AddSqlAgentAlert = Adding SQL Agent Alert '{0}'. - CreateAlertSetError = Unable to create the SQL Agent Alert '{0}' on {1}\\{2}. - DropAlertSetError = Unable to drop the SQL Agent Alert '{0}' on {1}\\{2}. - DeleteSqlAgentAlert = Deleting SQL Agent Alert '{0}'. - TestingConfiguration = Determines if the SQL Agent Alert is in the desired state. - ConnectServerFailed = Unable to connect to {0}\\{1}. - MultipleParameterError = Only one of Severity or MessageId can be specified, SQL Agent Alert '{0}' contains both. -'@ diff --git a/source/en-US/SqlAgentAlert.strings.psd1 b/source/en-US/SqlAgentAlert.strings.psd1 new file mode 100644 index 0000000000..43da4e143d --- /dev/null +++ b/source/en-US/SqlAgentAlert.strings.psd1 @@ -0,0 +1,16 @@ +<# + .SYNOPSIS + The localized resource strings in English (en-US) for the + resource SqlAgentAlert. +#> + +ConvertFrom-StringData @' + ## SqlAgentAlert + SqlAgentAlert_GettingCurrentState = Getting current state of SQL Agent Alert '{0}' on instance '{1}'. (SAAA0002) + SqlAgentAlert_AlertExists = SQL Agent Alert '{0}' exists. (SAAA0003) + SqlAgentAlert_AlertDoesNotExist = SQL Agent Alert '{0}' does not exist. (SAAA0004) + SqlAgentAlert_CreatingAlert = Creating SQL Agent Alert '{0}'. (SAAA0005) + SqlAgentAlert_UpdatingAlert = Updating SQL Agent Alert '{0}'. (SAAA0006) + SqlAgentAlert_RemovingAlert = Removing SQL Agent Alert '{0}'. (SAAA0007) + SqlAgentAlert_NoChangesNeeded = No changes needed for SQL Agent Alert '{0}'. (SAAA0009) +'@ diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 index 782f5a65b5..46da08f632 100644 --- a/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 +++ b/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 @@ -57,6 +57,7 @@ Describe "_Integration" -Tag @('Integration_SQL2016', ' Context ('When using configuration <_>') -ForEach @( "$($script:dscResourceName)_Add_Config" + "$($script:dscResourceName)_ChangeToMessageId_Config" ) { BeforeAll { $configurationName = $_ @@ -103,7 +104,15 @@ Describe "_Integration" -Tag @('Integration_SQL2016', ' $resourceCurrentState.Ensure | Should -Be 'Present' $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.Name - $resourceCurrentState.Severity | Should -Be $ConfigurationData.AllNodes.Severity + + if ($configurationName -eq "$($script:dscResourceName)_Add_Config") + { + $resourceCurrentState.Severity | Should -Be $ConfigurationData.AllNodes.Severity + } + elseif ($configurationName -eq "$($script:dscResourceName)_ChangeToMessageId_Config") + { + $resourceCurrentState.MessageId | Should -Be $ConfigurationData.AllNodes.MessageId + } } It 'Should return $true when Test-DscConfiguration is run' { @@ -158,7 +167,7 @@ Describe "_Integration" -Tag @('Integration_SQL2016', ' } $resourceCurrentState.Ensure | Should -Be 'Absent' - $resourceCurrentState.Name | Should -BeNullOrEmpty + $resourceCurrentState.Name | Should -Be $ConfigurationData.AllNodes.Name $resourceCurrentState.Severity | Should -BeNullOrEmpty } diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 index c039494a9d..70f55fa441 100644 --- a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 +++ b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 @@ -24,8 +24,9 @@ else ServerName = $env:COMPUTERNAME InstanceName = 'DSCSQLTEST' - Name = 'Sev17' + Name = 'MockAlert' Severity = '17' + MessageId = '50001' CertificateFile = $env:DscPublicCertificatePath } @@ -58,6 +59,68 @@ Configuration DSC_SqlAgentAlert_Add_Config } } +<# + .SYNOPSIS + Changes a SQL Agent alert to use MessageId instead of Severity. +#> +Configuration DSC_SqlAgentAlert_ChangeToMessageId_Config +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node $AllNodes.NodeName + { + # First, create the custom system message + SqlScriptQuery 'CreateCustomMessage' + { + Id = 'CreateCustomMessage' + InstanceName = $Node.InstanceName + ServerName = $Node.ServerName + Encrypt = 'Optional' + # cSpell: ignore addmessage msgnum msgtext + SetQuery = " + IF NOT EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) + BEGIN + EXEC sp_addmessage + @msgnum = $($Node.MessageId), + @severity = 16, + @msgtext = N'Custom test message for SqlAgentAlert integration test', + @lang = 'us_english' + END + " + TestQuery = " + IF NOT EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) + BEGIN + RAISERROR ('Did not found message id [$($Node.MessageId)]', 16, 1) + END + ELSE + BEGIN + PRINT 'Found a message id [$($Node.MessageId)]' + END + " + GetQuery = "SELECT message_id, text FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033 FOR JSON AUTO" + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + } + + SqlAgentAlert 'Integration_Test' + { + Ensure = 'Present' + ServerName = $Node.ServerName + InstanceName = $Node.InstanceName + Name = $Node.Name + MessageId = $Node.MessageId + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + + DependsOn = '[SqlScriptQuery]CreateCustomMessage' + } + } +} + <# .SYNOPSIS Removes a SQL Agent alert. @@ -74,13 +137,45 @@ Configuration DSC_SqlAgentAlert_Remove_Config ServerName = $Node.ServerName InstanceName = $Node.InstanceName Name = $Node.Name - Severity = $Node.Severity PsDscRunAsCredential = New-Object ` -TypeName System.Management.Automation.PSCredential ` -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) } - } -} + # Clean up the custom system message + SqlScriptQuery 'RemoveCustomMessage' + { + Id = 'RemoveCustomMessage' + InstanceName = $Node.InstanceName + ServerName = $Node.ServerName + Encrypt = 'Optional' + # cSpell: ignore dropmessage + SetQuery = " + IF EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) + BEGIN + EXEC sp_dropmessage + @msgnum = $($Node.MessageId), + @lang = 'us_english' + END + " + TestQuery = " + IF EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) + BEGIN + RAISERROR ('Found message id [$($Node.MessageId)]', 16, 1) + END + ELSE + BEGIN + PRINT 'Did not found a message id [$($Node.MessageId)]' + END + " + GetQuery = "SELECT message_id, text FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033 FOR JSON AUTO" + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + + DependsOn = '[SqlAgentAlert]Integration_Test' + } + } +} diff --git a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 new file mode 100644 index 0000000000..b170746de0 --- /dev/null +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -0,0 +1,663 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +[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 build" first.' + } +} + +BeforeAll { + $script:dscModuleName = 'SqlServerDsc' + + # Load SMO stub types + Add-Type -Path "$PSScriptRoot/../Stubs/SMO.cs" + + Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName + + # Set the environment variable for CI to suppress SMO warnings + $env:SqlServerDscCI = $true +} + +AfterAll { + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + # Remove the environment variable for CI + Remove-Item -Path 'env:SqlServerDscCI' -ErrorAction 'SilentlyContinue' + + # Unload the module being tested so that it doesn't impact any other tests. + Get-Module -Name $script:dscModuleName -All | Remove-Module -Force +} + +Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { + Context 'When using the constructor' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + $null = [SqlAgentAlert]::new() + } + } + + It 'Should have a default or empty constructor' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance | Should -Not -BeNullOrEmpty + } + } + + It 'Should be the correct type' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.GetType().Name | Should -Be 'SqlAgentAlert' + } + } + + It 'Should be able to set and get the Name property' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.Name = 'TestAlert' + $instance.Name | Should -Be 'TestAlert' + } + } + + It 'Should be able to set and get the InstanceName property' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.InstanceName = 'MSSQLSERVER' + $instance.InstanceName | Should -Be 'MSSQLSERVER' + } + } + + It 'Should be able to set and get the ServerName property' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.ServerName = 'TestServer' + $instance.ServerName | Should -Be 'TestServer' + } + } + + It 'Should be able to set and get the Ensure property' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.Ensure = 'Present' + $instance.Ensure | Should -Be 'Present' + } + } + + It 'Should be able to set and get the Severity property' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.Severity = 16 + $instance.Severity | Should -Be 16 + } + } + + It 'Should be able to set and get the MessageId property' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + $instance.MessageId = 50001 + $instance.MessageId | Should -Be 50001 + } + } + } + + Context 'When using the Get() method' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + $script:mockAlertObject = New-Object -TypeName 'PSCustomObject' + $script:mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlert' + $script:mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value 16 + # MessageId is not set when using Severity-based alerts + } + } + + It 'Should return current state when alert exists' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + return $script:mockAlertObject + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Severity = 17 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $result = $instance.Get() + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestAlert' + $result.Ensure | Should -Be 'Present' + $result.Severity | Should -Be 16 + } + } + + It 'Should return absent state when alert does not exist' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + return $null + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Severity = 17 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $result = $instance.Get() + + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be 'TestAlert' + $result.Ensure | Should -Be 'Absent' + } + } + } + + Context 'When using the Test() method' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } + } + + It 'Should return true when alert exists and is in desired state' { + InModuleScope -ScriptBlock { + $script:mockAlertObject = New-Object -TypeName 'PSCustomObject' + $script:mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlert' + $script:mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value 16 + $script:mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value 0 + + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + return $script:mockAlertObject + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Present' + Severity = 16 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $result = $instance.Test() + + $result | Should -BeTrue + } + } + + It 'Should return false when alert does not exist but should be present' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + return $null + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Present' + Severity = 16 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $result = $instance.Test() + + $result | Should -BeFalse + } + } + } + + Context 'When using the Set() method' { + Context 'When it does not exist and Ensure is Present' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } + + Mock -CommandName 'Get-SqlDscAgentAlert' + Mock -CommandName 'New-SqlDscAgentAlert' + Mock -CommandName 'Remove-SqlDscAgentAlert' + } + + It 'Should create alert' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Present' + Severity = 16 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $null = $instance.Set() + + Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + } + } + } + + Context 'When it exists and Ensure is Absent' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } + + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + $mockAlertObject = New-Object -TypeName 'PSCustomObject' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlert' + $mockAlertObject | Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value { } + + return $mockAlertObject + } + + Mock -CommandName 'New-SqlDscAgentAlert' + Mock -CommandName 'Remove-SqlDscAgentAlert' + } + + It 'Should remove alert' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Absent' + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $null = $instance.Set() + + Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 1 -Scope It + } + } + } + } + + Context 'When using the hidden Modify() method' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' + } + + Mock -CommandName 'New-SqlDscAgentAlert' + Mock -CommandName 'Remove-SqlDscAgentAlert' + Mock -CommandName 'Set-SqlDscAgentAlert' + } + + Context 'When Ensure is Present and alert exists' { + It 'Should update alert when Severity property differs' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + $mockAlertObject = New-Object -TypeName 'PSCustomObject' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlert' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value 10 + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value 0 + + return $mockAlertObject + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Present' + Severity = 16 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $properties = @{ + Severity = 16 + } + + $null = $instance.Modify($properties) + + Should -Invoke -CommandName 'Get-SqlDscAgentAlert' -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'Set-SqlDscAgentAlert' -ParameterFilter { + $AlertObject.Name -eq 'TestAlert' -and + $Severity -eq 16 + } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + } + } + + It 'Should update alert when MessageId property differs' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + $mockAlertObject = New-Object -TypeName 'PSCustomObject' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlert' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value 0 + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value 50001 + + return $mockAlertObject + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Present' + MessageId = 50002 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $properties = @{ + MessageId = 50002 + } + + $null = $instance.Modify($properties) + + Should -Invoke -CommandName 'Get-SqlDscAgentAlert' -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'Set-SqlDscAgentAlert' -ParameterFilter { + $AlertObject.Name -eq 'TestAlert' -and + $MessageId -eq 50002 + } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + } + } + + It 'Should not update alert when no properties differ' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Get-SqlDscAgentAlert' -MockWith { + $mockAlertObject = New-Object -TypeName 'PSCustomObject' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlert' + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value 16 + $mockAlertObject | Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value 0 + + return $mockAlertObject + } + + $instance = [SqlAgentAlert] @{ + Name = 'TestAlert' + InstanceName = 'MSSQLSERVER' + Ensure = 'Present' + Severity = 16 + } | + Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { + return $script:mockServerObject + } -PassThru + + $properties = @{ + Severity = 16 + } + + $null = $instance.Modify($properties) + + Should -Invoke -CommandName 'Get-SqlDscAgentAlert' -Exactly -Times 1 -Scope It + Should -Invoke -CommandName 'Set-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + } + } + } + } + + Context 'When using the hidden AssertProperties() method' { + Context 'When passing mutually exclusive parameters' { + Context 'When passing both Severity and MessageId' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + } + } + + It 'Should throw the correct error' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + MessageId = 50001 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*may be used at the same time*' + } + } + } + } + + Context 'When passing valid parameter combinations' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + } + } + + Context 'When Ensure is Present' { + It 'Should throw when neither Severity nor MessageId are specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0052)*' + } + } + + It 'Should not throw when only Severity is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } + + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) + } + } + + It 'Should not throw when only MessageId is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + MessageId = 50001 + } + + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) + } + } + } + + Context 'When Ensure is Absent' { + It 'Should not throw when only Name and Ensure are specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + } + + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) + } + } + } + } + + Context 'When passing invalid parameter combinations' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + } + } + + Context 'When Ensure is Absent and Severity or MessageId are specified' { + It 'Should throw the correct error when Severity is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + Severity = 16 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0053)*' + } + } + + It 'Should throw the correct error when MessageId is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + MessageId = 50001 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0053)*' + } + } + + It 'Should throw the correct error when both Severity and MessageId are specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + Severity = 16 + MessageId = 50001 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0053)*' + } + } + } + } + + Context 'When validating Assert-BoundParameter calls' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + } + } + + Context 'When Ensure is Present' { + It 'Should call Assert-BoundParameter to validate at least one of Severity or MessageId is specified' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Assert-BoundParameter' -MockWith { } + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } + + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) + + Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { + $BoundParameterList -is [hashtable] -and + $AtLeastOneList -contains 'Severity' -and + $AtLeastOneList -contains 'MessageId' -and + $IfEqualParameterList.Ensure -eq 'Present' + } -Exactly -Times 1 -Scope It + } + } + + It 'Should call Assert-BoundParameter to validate Severity and MessageId are mutually exclusive' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Assert-BoundParameter' -MockWith { } + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } + + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) + + Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { + $BoundParameterList -is [hashtable] -and + $MutuallyExclusiveList1 -contains 'Severity' -and + $MutuallyExclusiveList2 -contains 'MessageId' -and + $IfEqualParameterList.Ensure -eq 'Present' + } -Exactly -Times 1 -Scope It + } + } + } + + Context 'When Ensure is Absent' { + It 'Should call Assert-BoundParameter to validate Severity and MessageId are not allowed' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Assert-BoundParameter' -MockWith { } + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + } + + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) + + Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { + $BoundParameterList -is [hashtable] -and + $NotAllowedList -contains 'Severity' -and + $NotAllowedList -contains 'MessageId' -and + $IfEqualParameterList.Ensure -eq 'Absent' + } -Exactly -Times 1 -Scope It + } + } + + It 'Should call Assert-BoundParameter with correct parameters when Severity is specified' { + InModuleScope -ScriptBlock { + Mock -CommandName 'Assert-BoundParameter' -MockWith { + # Simulate the Assert-BoundParameter throwing an exception for NotAllowed parameters + if ($NotAllowedList -and ($BoundParameterList.ContainsKey('Severity') -or $BoundParameterList.ContainsKey('MessageId'))) { + throw 'Parameter validation failed' + } + } + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + Severity = 16 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw + + Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { + $BoundParameterList -is [hashtable] -and + $NotAllowedList -contains 'Severity' -and + $NotAllowedList -contains 'MessageId' -and + $IfEqualParameterList.Ensure -eq 'Absent' + } -Exactly -Times 1 -Scope It + } + } + } + } + } +} diff --git a/tests/Unit/DSC_SqlAgentAlert.Tests.ps1 b/tests/Unit/DSC_SqlAgentAlert.Tests.ps1 deleted file mode 100644 index 13a14fb430..0000000000 --- a/tests/Unit/DSC_SqlAgentAlert.Tests.ps1 +++ /dev/null @@ -1,842 +0,0 @@ -<# - .SYNOPSIS - Unit test for DSC_SqlAgentAlert DSC resource. -#> - -# Suppressing this rule because Script Analyzer does not understand Pester's syntax. -[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 build" first.' - } -} - -BeforeAll { - $script:dscModuleName = 'SqlServerDsc' - $script:dscResourceName = 'DSC_SqlAgentAlert' - - $env:SqlServerDscCI = $true - - $script:testEnvironment = Initialize-TestEnvironment ` - -DSCModuleName $script:dscModuleName ` - -DSCResourceName $script:dscResourceName ` - -ResourceType 'Mof' ` - -TestType 'Unit' - - Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') - - $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscResourceName - $PSDefaultParameterValues['Mock:ModuleName'] = $script:dscResourceName - $PSDefaultParameterValues['Should:ModuleName'] = $script:dscResourceName -} - -AfterAll { - $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') - $PSDefaultParameterValues.Remove('Mock:ModuleName') - $PSDefaultParameterValues.Remove('Should:ModuleName') - - Restore-TestEnvironment -TestEnvironment $script:testEnvironment - - # Unload the module being tested so that it doesn't impact any other tests. - Get-Module -Name $script:dscResourceName -All | Remove-Module -Force - - # Remove module common test helper. - Get-Module -Name 'CommonTestHelper' -All | Remove-Module -Force - - Remove-Item -Path 'env:SqlServerDscCI' -} - -Describe 'DSC_SqlAgentAlert\Get-TargetResource' -Tag 'Get' { - BeforeAll { - InModuleScope -ScriptBlock { - $script:mockServerName = 'localhost' - $script:mockInstanceName = 'MSSQLSERVER' - - # Default parameters that are used for the It-blocks - $script:mockDefaultParameters = @{ - InstanceName = $mockInstanceName - ServerName = $mockServerName - } - } - - # Mocked object for Connect-SQL. - $mockConnectSQL = { - return @( - ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'ScriptProperty' -Name 'JobServer' -Value { - return ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'ScriptProperty' -Name 'Alerts' -Value { - return @( - ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlertSev' -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value '17' -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value 0 -PassThru -Force - ), - ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlertMsg' -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value 0 -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value '825' -PassThru -Force - ) - ) - } -PassThru - ) - } -PassThru -Force - ) - ) - } - } - - Context 'When Connect-SQL returns nothing' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return $null - } - } - - It 'Should throw the correct error' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters.Clone() - $mockTestParameters.Name = 'TestAlertSev' - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $script:localizedData.ConnectServerFailed -f $mockTestParameters.ServerName, $mockTestParameters.InstanceName - ) - - { Get-TargetResource @mockTestParameters } | - Should -Throw -ExpectedMessage $mockErrorRecord.Exception.Message - } - } - } - - Context 'When the system is not in the desired state' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable - - InModuleScope -ScriptBlock { - $script:mockTestParameters = $mockDefaultParameters.Clone() - $script:mockTestParameters.Name = 'MissingAlert' - } - } - - It 'Should return the state as absent' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $result = Get-TargetResource @mockTestParameters - - $result.Ensure | Should -Be 'Absent' - } - - Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It - } - - It 'Should return the same values as passed as parameters' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $result = Get-TargetResource @mockTestParameters - - $result.ServerName | Should -Be $mockTestParameters.ServerName - $result.InstanceName | Should -Be $mockTestParameters.InstanceName - } - } - - It 'Should call the mock function Connect-SQL' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - { Get-TargetResource @mockTestParameters } | Should -Not -Throw - } - - Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It - } - - It 'Should call all verifiable mocks' { - Should -InvokeVerifiable - } - } - - Context 'When the system is in the desired state for a sql agent alert' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable - - InModuleScope -ScriptBlock { - $script:mockTestParameters = $mockDefaultParameters.Clone() - $script:mockTestParameters.Name = 'TestAlertSev' - } - } - - It 'Should return the state as present' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $result = Get-TargetResource @mockTestParameters - - $result.Ensure | Should -Be 'Present' - } - - Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It - } - - It 'Should return the same values as passed as parameters' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $result = Get-TargetResource @mockTestParameters - - $result.ServerName | Should -Be $mockTestParameters.ServerName - $result.InstanceName | Should -Be $mockTestParameters.InstanceName - $result.Name | Should -Be $mockTestParameters.Name - $result.Severity | Should -Be '17' - $result.MessageId | Should -Be 0 - } - } - - It 'Should call all verifiable mocks' { - Should -InvokeVerifiable - } - } -} - -Describe 'DSC_SqlAgentAlert\Test-TargetResource' -Tag 'Test' { - BeforeAll { - InModuleScope -ScriptBlock { - $script:mockServerName = 'localhost' - $script:mockInstanceName = 'MSSQLSERVER' - - # Default parameters that are used for the It-blocks - $script:mockDefaultParameters = @{ - InstanceName = $mockInstanceName - ServerName = $mockServerName - } - } - } - - Context 'When the system is not in the desired state' { - Context 'When the alert does not exist' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - Name = $null - Ensure = 'Absent' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = $null - MessageId = $null - } - } - } - - It 'Should return the state as false' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'MissingAlert' - Severity = '25' - Ensure = 'Present' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeFalse - } - - Should -Invoke -CommandName Get-TargetResource -Exactly -Times 1 -Scope It - } - } - - Context 'When the alert should not exist' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - Name = 'TestAlertSev' - Ensure = 'Present' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = '17' - MessageId = '825' - } - } - } - - It 'Should return the state as false' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Absent' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeFalse - } - - Should -Invoke -CommandName Get-TargetResource -Exactly -Times 1 -Scope It - } - } - - Context 'When enforcing a severity alert' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - Name = 'TestAlertSev' - Ensure = 'Present' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = '17' - MessageId = '825' - } - } - } - - It 'Should return the state as false when desired sql agent alert exists but has the incorrect severity' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Present' - Severity = '25' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeFalse - } - } - - It 'Should return the state as false when desired sql agent alert exists but has the incorrect message id' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Present' - MessageId = '500' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeFalse - } - } - } - } - - Context 'When the system is in the desired state' { - Context 'When an alert does not exist' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - Name = 'MissingAlert' - Ensure = 'Absent' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = $null - MessageId = $null - } - } - } - - It 'Should return the state as true' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'MissingAlert' - Ensure = 'Absent' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeTrue - } - } - } - - Context 'When enforcing a severity alert' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - Name = 'TestAlertSev' - Ensure = 'Present' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = '17' - MessageId = '825' - } - } - } - - It 'Should return the state as true when desired sql agent alert exist' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Present' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeTrue - } - - Should -Invoke -CommandName Get-TargetResource -Exactly -Times 1 -Scope It - } - - It 'Should return the state as true when desired sql agent alert exists and has the correct severity' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Present' - Severity = '17' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeTrue - } - } - } - - Context 'When enforcing a message alert' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - Name = 'TestAlertMsg' - Ensure = 'Present' - ServerName = $ServerName - InstanceName = $InstanceName - Severity = '17' - MessageId = '825' - } - } - } - - It 'Should return the state as true when desired sql agent alert exists and has the correct message id' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertMsg' - Ensure = 'Present' - MessageId = '825' - } - - $result = Test-TargetResource @mockTestParameters - - $result | Should -BeTrue - } - } - } - } -} - -Describe 'DSC_SqlAgentAlert\Set-TargetResource' -Tag 'Set' { - BeforeAll { - InModuleScope -ScriptBlock { - $script:mockServerName = 'localhost' - $script:mockInstanceName = 'MSSQLSERVER' - - # Default parameters that are used for the It-blocks - $script:mockDefaultParameters = @{ - InstanceName = $mockInstanceName - ServerName = $mockServerName - } - } - - $mockInvalidOperationForCreateMethod = $false - $mockInvalidOperationForDropMethod = $false - $mockInvalidOperationForAlterMethod = $false - $mockExpectedSqlAgentAlertToCreate = 'MissingAlertSev' - $mockExpectedSqlAgentAlertToDrop = 'Sev18' - - # Mocked object for Connect-SQL. - $mockConnectSQL = { - return @( - ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'ScriptProperty' -Name 'JobServer' -Value { - return ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'ScriptProperty' -Name 'Alerts' -Value { - return @( - ( - New-Object -TypeName 'Object' | - Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestAlertSev' -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value '17' -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value 0 -PassThru | - Add-Member -MemberType 'ScriptMethod' -Name 'Drop' -Value { - if ($mockInvalidOperationForDropMethod) - { - throw 'Mock Drop Method was called with invalid operation.' - } - - if ( $this.Name -ne $mockExpectedSqlAgentAlertToDrop ) - { - throw "Called mocked Drop() method without dropping the right sql agent alert. Expected '{0}'. But was '{1}'." ` - -f $mockExpectedSqlAgentAlertToDrop, $this.Name - } - } -PassThru | - Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { - if ($mockInvalidOperationForAlterMethod) - { - throw 'Mock Alter Method was called with invalid operation.' - } - } -PassThru -Force - ), - ( - New-Object -TypeName 'Object' | - Add-Member -MemberType NoteProperty -Name 'Name' -Value 'TestAlertMsg' -PassThru | - Add-Member -MemberType NoteProperty -Name 'Severity' -Value 0 -PassThru | - Add-Member -MemberType NoteProperty -Name 'MessageId' -Value '825' -PassThru | - Add-Member -MemberType ScriptMethod -Name 'Drop' -Value { - if ($mockInvalidOperationForDropMethod) - { - throw 'Mocking that the method Drop is throwing an invalid operation.' - } - - if ($this.Name -ne $mockExpectedSqlAgentAlertToDrop) - { - throw "Called mocked Drop() method without dropping the right sql agent alert. Expected '{0}'. But was '{1}'." ` - -f $mockExpectedSqlAgentAlertToDrop, $this.Name - } - } -PassThru | - Add-Member -MemberType 'ScriptMethod' -Name 'Alter' -Value { - if ($mockInvalidOperationForAlterMethod) - { - throw 'Mock Alter Method was called with invalid operation.' - } - - if ($this.MessageId -eq 7) - { - throw "Called mocked Create() method for a message id that doesn't exist." - } - - if ($this.Severity -eq 999) - { - throw "Called mocked Create() method for a severity that doesn't exist." - } - } -PassThru -Force - ) - ) - } -PassThru - ) - } -PassThru -Force - ) - ) - } - - <# - Mocked object for New-Object with parameter filter - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Agent.Alert'. - #> - $mockNewSqlAgentAlert = { - return @( - ( - New-Object -TypeName 'Object' | - # Using the value from the second property passed in the parameter ArgumentList of the cmdlet New-Object. - Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value $ArgumentList[1] -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'Severity' -Value $null -PassThru | - Add-Member -MemberType 'NoteProperty' -Name 'MessageId' -Value $null -PassThru | - Add-Member -MemberType 'ScriptMethod' -Name 'Create' -Value { - if ($mockInvalidOperationForCreateMethod) - { - throw 'Mocking that the method Create is throwing an invalid operation.' - } - - if ($this.Name -ne $mockExpectedSqlAgentAlertToCreate) - { - throw "Called mocked Create() method without adding the right sql agent alert. Expected '{0}'. But was '{1}'." ` - -f $mockExpectedSqlAgentAlertToCreate, $this.Name - } - } -PassThru -Force - ) - ) - } - } - - Context 'When Connect-SQL returns nothing' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith { - return $null - } - } - - It 'Should throw the correct error' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'Message7' - Ensure = 'Present' - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $script:localizedData.ConnectServerFailed -f $mockTestParameters.ServerName, $mockTestParameters.InstanceName - ) - - { Set-TargetResource @mockTestParameters } | - Should -Throw -ExpectedMessage $mockErrorRecord.Exception.Message - } - } - } - - Context 'When the system is not in the desired state and Ensure is set to Present' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable - Mock -CommandName New-Object -MockWith $mockNewSqlAgentAlert -ParameterFilter { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Agent.Alert' - } -Verifiable - } - - It 'Should not throw when creating the sql agent alert' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'MissingAlertSev' - Ensure = 'Present' - Severity = '16' - } - - { Set-TargetResource @mockTestParameters } | Should -Not -Throw - } - - Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It - Should -Invoke -CommandName New-Object -ParameterFilter { - $TypeName -eq 'Microsoft.SqlServer.Management.Smo.Agent.Alert' - } -Exactly -Times 1 -Scope It - } - - It 'Should not throw when changing the severity' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Present' - Severity = '17' - } - - { Set-TargetResource @mockTestParameters } | Should -Not -Throw - } - } - - It 'Should not throw when changing the message id' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertMsg' - Ensure = 'Present' - MessageId = '825' - } - - { Set-TargetResource @mockTestParameters } | Should -Not -Throw - } - } - - It 'Should throw when changing severity and message id' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertMsg' - Ensure = 'Present' - Severity = '17' - MessageId = '825' - } - - { Set-TargetResource @mockTestParameters } | Should -Throw - } - } - - It 'Should throw when message id is not valid when altering existing alert' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertMsg' - Ensure = 'Present' - MessageId = '7' - } - - { Set-TargetResource @mockTestParameters } | Should -Throw - } - } - - It 'Should throw when severity is not valid when altering existing alert' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertMsg' - Ensure = 'Present' - Severity = '999' - } - - { Set-TargetResource @mockTestParameters } | Should -Throw - } - } - - It 'Should throw when message id is not valid when creating alert' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'NewAlertMsg' - Ensure = 'Present' - MessageId = '7' - } - - { Set-TargetResource @mockTestParameters } | Should -Throw - } - } - - It 'Should throw when severity is not valid when creating alert' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'NewAlertMsg' - Ensure = 'Present' - Severity = '999' - } - - { Set-TargetResource @mockTestParameters } | Should -Throw - } - } - - It 'Should throw the correct error when Create() method was called with invalid operation' { - $mockInvalidOperationForCreateMethod = $true - - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'NewAlert' - Ensure = 'Present' - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $script:localizedData.CreateAlertSetError -f $mockTestParameters.Name, $mockTestParameters.ServerName, $mockTestParameters.InstanceName - ) - - <# - Using wildcard for comparison due to that the mock throws and adds - the mocked exception message on top of the original message. - #> - { Set-TargetResource @mockTestParameters } | - Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - } - - $mockInvalidOperationForCreateMethod = $false - } - - It 'Should call all verifiable mocks' { - Should -InvokeVerifiable - } - } - - Context 'When the system is not in the desired state and Ensure is set to Absent' { - BeforeAll { - Mock -CommandName Connect-SQL -MockWith $mockConnectSQL -Verifiable - } - - It 'Should not throw when dropping the sql agent alert' { - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'Sev16' - Ensure = 'Absent' - } - - { Set-TargetResource @mockTestParameters } | Should -Not -Throw - } - - Should -Invoke -CommandName Connect-SQL -Exactly -Times 1 -Scope It - } - - It 'Should throw the correct error when Drop() method was called with invalid operation' { - $mockInvalidOperationForDropMethod = $true - - InModuleScope -ScriptBlock { - Set-StrictMode -Version 1.0 - - $mockTestParameters = $mockDefaultParameters - $mockTestParameters += @{ - Name = 'TestAlertSev' - Ensure = 'Absent' - } - - $mockErrorRecord = Get-InvalidOperationRecord -Message ( - $script:localizedData.DropAlertSetError -f $mockTestParameters.Name, $mockTestParameters.ServerName, $mockTestParameters.InstanceName - ) - - <# - Using wildcard for comparison due to that the mock throws and adds - the mocked exception message on top of the original message. - #> - { Set-TargetResource @mockTestParameters } | - Should -Throw -ExpectedMessage ($mockErrorRecord.Exception.Message + '*') - } - - $mockInvalidOperationForDropMethod = $false - } - - It 'Should call all verifiable mocks' { - Should -InvokeVerifiable - } - } -} diff --git a/tests/Unit/Stubs/SMO.cs b/tests/Unit/Stubs/SMO.cs index e2089ab3da..003c52987f 100644 --- a/tests/Unit/Stubs/SMO.cs +++ b/tests/Unit/Stubs/SMO.cs @@ -1507,6 +1507,7 @@ namespace Microsoft.SqlServer.Management.Smo.Agent // Set-SqlDscAgentAlert.Tests.ps1 // Remove-SqlDscAgentAlert.Tests.ps1 // Test-SqlDscAgentAlert.Tests.ps1 + // SqlAgentAlert.Tests.ps1 public enum AlertType { SqlServerEvent = 1, @@ -1518,6 +1519,7 @@ public enum AlertType // TypeName: Microsoft.SqlServer.Management.Smo.Agent.CompletionAction // Used by: // SQL Agent Alert commands unit tests + // SqlAgentAlert.Tests.ps1 public enum CompletionAction { Never = 0, @@ -1533,6 +1535,7 @@ public enum CompletionAction // TypeName: Microsoft.SqlServer.Management.Smo.Agent.JobServer // Used by: // SQL Agent Alert commands unit tests + // SqlAgentAlert.Tests.ps1 public class JobServer { // Constructor @@ -1558,6 +1561,7 @@ public static JobServer CreateTypeInstance() // TypeName: Microsoft.SqlServer.Management.Smo.Agent.AlertCollection // Used by: // SQL Agent Alert commands unit tests + // SqlAgentAlert.Tests.ps1 public class AlertCollection : ICollection { private System.Collections.Generic.Dictionary alerts = new System.Collections.Generic.Dictionary();