From 4d5b237ce1288cfacc55a6b0e11c0c3fcae92526 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Sun, 24 Aug 2025 15:02:30 +0200 Subject: [PATCH 01/37] `SqlAgentAlert`: Refactor to class-bases resource --- .../SqlServerDsc-guidelines.instructions.md | 7 +- ...-guidelines-class-resource.instructions.md | 27 ++ ...ty-style-guidelines-pester.instructions.md | 2 + ...tyle-guidelines-powershell.instructions.md | 2 +- CHANGELOG.md | 2 + source/Classes/020.SqlAgentAlert.ps1 | 281 +++++++++++++ .../DSC_SqlAgentAlert/DSC_SqlAgentAlert.psm1 | 372 ----------------- .../DSC_SqlAgentAlert.schema.mof | 10 - .../DSCResources/DSC_SqlAgentAlert/README.md | 13 - .../en-US/DSC_SqlAgentAlert.strings.psd1 | 18 - source/en-US/SqlAgentAlert.strings.psd1 | 16 + tests/Unit/Classes/SqlAgentAlert.Tests.ps1 | 382 ++++++++++++++++++ tests/Unit/Stubs/SMO.cs | 4 + 13 files changed, 719 insertions(+), 417 deletions(-) create mode 100644 source/Classes/020.SqlAgentAlert.ps1 delete mode 100644 source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.psm1 delete mode 100644 source/DSCResources/DSC_SqlAgentAlert/DSC_SqlAgentAlert.schema.mof delete mode 100644 source/DSCResources/DSC_SqlAgentAlert/README.md delete mode 100644 source/DSCResources/DSC_SqlAgentAlert/en-US/DSC_SqlAgentAlert.strings.psd1 create mode 100644 source/en-US/SqlAgentAlert.strings.psd1 create mode 100644 tests/Unit/Classes/SqlAgentAlert.Tests.ps1 diff --git a/.github/instructions/SqlServerDsc-guidelines.instructions.md b/.github/instructions/SqlServerDsc-guidelines.instructions.md index 5202dbd54a..a080cf8e25 100644 --- a/.github/instructions/SqlServerDsc-guidelines.instructions.md +++ b/.github/instructions/SqlServerDsc-guidelines.instructions.md @@ -34,6 +34,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-class-resource.instructions.md b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md index e018d98166..698fbd9cc6 100644 --- a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md @@ -12,6 +12,17 @@ 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 +- Properties must have nullable types (`[Nullable[{Type}]]` where needed) + +## Required constructor + +```powershell +MyResourceName () : base () +{ + # Property names where state cannot be enforced, e.g. IsSingelInstance + $this.ExcludeDscProperties = @() +} +``` ## Required constructor @@ -78,3 +89,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-pester.instructions.md b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md index fc9c2fe2fe..5e6e387abd 100644 --- a/.github/instructions/dsc-community-style-guidelines-pester.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md @@ -29,11 +29,13 @@ applyTo: "**/*.[Tt]ests.ps1" - `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..dde0765dae 100644 --- a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md @@ -79,7 +79,7 @@ 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 `$PSCmdlet.ThrowTerminatingError()` for terminating errors (except for classes), 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 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..71acd76bdd --- /dev/null +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -0,0 +1,281 @@ +<# + .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. + + 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 also possible to instead use impersonation by the + parameter **Credential**. + + ## 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 using advanced type like the parameter + **Reasons** have. + + ### Using **Credential** property + + SQL Authentication and Group Managed Service Accounts is not supported as + impersonation credentials. Currently only Windows Integrated Security is + supported to use as credentials. + + For Windows Authentication the username must either be provided with the User + Principal Name (UPN), e.g. `username@domain.local` or if using non-domain + (for example a local Windows Server account) account the username must be + provided without the NetBIOS name, e.g. `username`. Using the NetBIOS name, e.g + using the format `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 0 to 25. + Cannot be used together with **MessageId**. + + .PARAMETER MessageId + The message id of the _SQL Server Agent_ alert. Valid range is 0 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(0, 25)] + [Nullable[System.Int32]] + $Severity + + [DscProperty()] + [ValidateRange(0, 2147483647)] + [Nullable[System.Int32]] + $MessageId + + SqlAgentAlert () : base () + { + # Property names that cannot be enforced, e.g. Ensure + $this.ExcludeDscProperties = @() + } + + [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 both Severity and MessageId are not specified + Assert-BoundParameter -BoundParameterList $properties -MutuallyExclusiveList1 @('Severity') -MutuallyExclusiveList2 @('MessageId') + } + + 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/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 new file mode 100644 index 0000000000..9f75793ceb --- /dev/null +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -0,0 +1,382 @@ +[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 class is instantiated' { + It 'Should not throw an exception' { + InModuleScope -ScriptBlock { + { [SqlAgentAlert]::new() } | Should -Not -Throw + } + } + + 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' + } + } + } + + Context 'When setting and getting properties' { + 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 testing 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' + } | + 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' + } | + 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 testing 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 testing 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 + + { $instance.Set() } | Should -Not -Throw + + 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 + + { $instance.Set() } | Should -Not -Throw + + Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 0 -Scope It + Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 1 -Scope It + } + } + } + } + + Context 'When testing AssertProperties() method' { + It 'Should throw an error when both Severity and MessageId are specified' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + MessageId = 50001 + } + + { $instance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*may be used at the same time*' + } + } + + It 'Should not throw when neither Severity nor MessageId are specified' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + } + + { $instance.AssertProperties($properties) } | Should -Not -Throw + } + } + + It 'Should not throw when only Severity is specified' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } + + { $instance.AssertProperties($properties) } | Should -Not -Throw + } + } + + It 'Should not throw when only MessageId is specified' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + MessageId = 50001 + } + + { $instance.AssertProperties($properties) } | Should -Not -Throw + } + } + + It 'Should not throw when Ensure is Absent' { + InModuleScope -ScriptBlock { + $instance = [SqlAgentAlert]::new() + + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + } + + { $instance.AssertProperties($properties) } | Should -Not -Throw + } + } + } +} 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(); From 4897a08bd70dcef991ee82f8e9b9473060fd8ff2 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Mon, 25 Aug 2025 12:11:51 +0200 Subject: [PATCH 02/37] Fix typo in DSC class resource guidelines for ExcludeDscProperties comment --- ...sc-community-style-guidelines-class-resource.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 698fbd9cc6..d1782663ea 100644 --- a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md @@ -19,7 +19,7 @@ applyTo: "source/[cC]lasses/**/*.ps1" ```powershell MyResourceName () : base () { - # Property names where state cannot be enforced, e.g. IsSingelInstance + # Property names where state cannot be enforced, e.g. IsSingleInstance, Force $this.ExcludeDscProperties = @() } ``` From 11a2f0010d61cc9b0231389cab17960a3e0a0967 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 10:01:21 +0200 Subject: [PATCH 03/37] Remove unit tests for DSC_SqlAgentAlert resource --- tests/Unit/DSC_SqlAgentAlert.Tests.ps1 | 842 ------------------------- 1 file changed, 842 deletions(-) delete mode 100644 tests/Unit/DSC_SqlAgentAlert.Tests.ps1 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 - } - } -} From e62febdbaecd0feb4d43aa4a722c7810d20882b9 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 13:34:21 +0200 Subject: [PATCH 04/37] Update SqlServerDsc guidelines to clarify resource inheritance and properties --- .github/instructions/SqlServerDsc-guidelines.instructions.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/instructions/SqlServerDsc-guidelines.instructions.md b/.github/instructions/SqlServerDsc-guidelines.instructions.md index a080cf8e25..d4a96f95d3 100644 --- a/.github/instructions/SqlServerDsc-guidelines.instructions.md +++ b/.github/instructions/SqlServerDsc-guidelines.instructions.md @@ -11,7 +11,8 @@ applyTo: "**" ## Resources - Database Engine resources: inherit `SqlResourceBase` -- `SqlResourceBase` provides: `InstanceName`, `ServerName`, `Credential`, `Reasons`, `GetServerObject()` + - Inheriting `SqlResourceBase`; add `InstanceName`, `ServerName`, and `Credential` to `$this.ExcludeDscProperties` + - `SqlResourceBase` provides: `InstanceName`, `ServerName`, `Credential`, `Reasons`, `GetServerObject()` ## SQL Server Interaction - Always prefer SMO over T-SQL From 4dfbc14b156d41e91cac714b6dd4106aa38096cd Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 13:44:49 +0200 Subject: [PATCH 05/37] Update ExcludeDscProperties in SqlAgentAlert constructor to include InstanceName, ServerName, and Credential --- source/Classes/020.SqlAgentAlert.ps1 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index 71acd76bdd..b12528633c 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -136,8 +136,12 @@ class SqlAgentAlert : SqlResourceBase SqlAgentAlert () : base () { - # Property names that cannot be enforced, e.g. Ensure - $this.ExcludeDscProperties = @() + # Property names that cannot be enforced + $this.ExcludeDscProperties = @( + 'InstanceName', + 'ServerName', + 'Credential' + ) } [SqlAgentAlert] Get() From 642a74220bc2e50fd652abcb09696a2a9e390d59 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 13:45:01 +0200 Subject: [PATCH 06/37] Remove unnecessary blank lines from DSC_SqlAgentAlert_Remove_Config configuration --- tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 index c039494a9d..5d3fc0a12e 100644 --- a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 +++ b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 @@ -82,5 +82,3 @@ Configuration DSC_SqlAgentAlert_Remove_Config } } } - - From d74eedd75c703f199cbb301173493dfd1feaf708 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 13:45:07 +0200 Subject: [PATCH 07/37] Update integration test to verify resource name against ConfigurationData --- .../Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 index 782f5a65b5..e8c72d5ab4 100644 --- a/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 +++ b/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 @@ -158,7 +158,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 } From 9398b1d03ce7de5d1fc91f22dd8aef55435656c2 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 14:03:36 +0200 Subject: [PATCH 08/37] Update AssertProperties method to validate Severity and MessageId parameters conditionally --- source/Classes/020.SqlAgentAlert.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index b12528633c..6ef31de9e3 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -168,8 +168,16 @@ class SqlAgentAlert : SqlResourceBase hidden [void] AssertProperties([System.Collections.Hashtable] $properties) { - # Validate that both Severity and MessageId are not specified - Assert-BoundParameter -BoundParameterList $properties -MutuallyExclusiveList1 @('Severity') -MutuallyExclusiveList2 @('MessageId') + # TODO: Waiting for issue: https://github.com/dsccommunity/DscResource.Common/issues/160 + if ($this.Ensure -eq 'Present') + { + # Validate that at least one of Severity or MessageId is specified + # TODO: Waiting for issue: https://github.com/dsccommunity/DscResource.Common/issues/161 + #Assert-BoundParameter -BoundParameterList $properties -AtLeastOneList @('Severity', 'MessageId') + + # Validate that both Severity and MessageId are not specified + Assert-BoundParameter -BoundParameterList $properties -MutuallyExclusiveList1 @('Severity') -MutuallyExclusiveList2 @('MessageId') + } } hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) From 9a5cba321c01fbf86220a119a9675d6d6acb0848 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 14:03:53 +0200 Subject: [PATCH 09/37] Fix AssertProperties method to validate Ensure property from input parameters --- source/Classes/020.SqlAgentAlert.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index 6ef31de9e3..023df67217 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -169,7 +169,7 @@ class SqlAgentAlert : SqlResourceBase hidden [void] AssertProperties([System.Collections.Hashtable] $properties) { # TODO: Waiting for issue: https://github.com/dsccommunity/DscResource.Common/issues/160 - if ($this.Ensure -eq 'Present') + if ($properties.Ensure -eq 'Present') { # Validate that at least one of Severity or MessageId is specified # TODO: Waiting for issue: https://github.com/dsccommunity/DscResource.Common/issues/161 From 017f3a873c8432940b5d80780674180c2635abfa Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 16:52:31 +0200 Subject: [PATCH 10/37] Enhance SqlAgentAlert to restrict Severity and MessageId when Ensure is set to 'Absent' and update tests accordingly --- source/Classes/020.SqlAgentAlert.ps1 | 11 ++ source/en-US/SqlAgentAlert.strings.psd1 | 1 + tests/Unit/Classes/SqlAgentAlert.Tests.ps1 | 141 +++++++++++++++------ 3 files changed, 111 insertions(+), 42 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index 023df67217..40bab81b90 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -141,6 +141,7 @@ class SqlAgentAlert : SqlResourceBase 'InstanceName', 'ServerName', 'Credential' + 'Name' ) } @@ -178,6 +179,16 @@ class SqlAgentAlert : SqlResourceBase # Validate that both Severity and MessageId are not specified Assert-BoundParameter -BoundParameterList $properties -MutuallyExclusiveList1 @('Severity') -MutuallyExclusiveList2 @('MessageId') } + else + { + # When Ensure is 'Absent', Severity and MessageId must not be set + if ($properties.ContainsKey('Severity') -or $properties.ContainsKey('MessageId')) + { + $errorMessage = $this.localizedData.SqlAgentAlert_SeverityOrMessageIdNotAllowedWhenAbsent + + New-InvalidArgumentException -ArgumentName 'Severity, MessageId' -Message $errorMessage + } + } } hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) diff --git a/source/en-US/SqlAgentAlert.strings.psd1 b/source/en-US/SqlAgentAlert.strings.psd1 index 43da4e143d..ef89513300 100644 --- a/source/en-US/SqlAgentAlert.strings.psd1 +++ b/source/en-US/SqlAgentAlert.strings.psd1 @@ -12,5 +12,6 @@ ConvertFrom-StringData @' 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_SeverityOrMessageIdNotAllowedWhenAbsent = Cannot specify Severity or MessageId when Ensure is set to 'Absent'. (SAAA0008) SqlAgentAlert_NoChangesNeeded = No changes needed for SQL Agent Alert '{0}'. (SAAA0009) '@ diff --git a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 index 9f75793ceb..b854ddee84 100644 --- a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -309,73 +309,130 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } - Context 'When testing AssertProperties() method' { - It 'Should throw an error when both Severity and MessageId are specified' { - InModuleScope -ScriptBlock { - $instance = [SqlAgentAlert]::new() - - $properties = @{ - Name = 'TestAlert' - Ensure = 'Present' - Severity = 16 - MessageId = 50001 + Context 'When passing mutually exclusive parameters' { + Context 'When passing both Severity and MessageId' { + BeforeAll { + InModuleScope -ScriptBlock { + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() } + } - { $instance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*may be used at the same time*' + 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*' + } } } + } - It 'Should not throw when neither Severity nor MessageId are specified' { + Context 'When passing valid parameter combinations' { + BeforeAll { InModuleScope -ScriptBlock { - $instance = [SqlAgentAlert]::new() + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + } + } - $properties = @{ - Name = 'TestAlert' - Ensure = 'Present' - } + Context 'When Ensure is Present' { + It 'Should not throw when neither Severity nor MessageId are specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + } - { $instance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + } } - } - It 'Should not throw when only Severity is specified' { - InModuleScope -ScriptBlock { - $instance = [SqlAgentAlert]::new() + It 'Should not throw when only Severity is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } - $properties = @{ - Name = 'TestAlert' - Ensure = 'Present' - Severity = 16 + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw } + } - { $instance.AssertProperties($properties) } | Should -Not -Throw + It 'Should not throw when only MessageId is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + MessageId = 50001 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + } } } - It 'Should not throw when only MessageId is specified' { - InModuleScope -ScriptBlock { - $instance = [SqlAgentAlert]::new() + Context 'When Ensure is Absent' { + It 'Should not throw when only Name and Ensure are specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + } - $properties = @{ - Name = 'TestAlert' - Ensure = 'Present' - MessageId = 50001 + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw } - - { $instance.AssertProperties($properties) } | Should -Not -Throw } } + } - It 'Should not throw when Ensure is Absent' { + Context 'When passing invalid parameter combinations' { + BeforeAll { InModuleScope -ScriptBlock { - $instance = [SqlAgentAlert]::new() + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + } + } - $properties = @{ - Name = 'TestAlert' - Ensure = 'Absent' + 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 '*Cannot specify Severity or MessageId when Ensure is set to ''Absent''*' + } + } + + 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 '*Cannot specify Severity or MessageId when Ensure is set to ''Absent''*' } + } - { $instance.AssertProperties($properties) } | Should -Not -Throw + 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 '*Cannot specify Severity or MessageId when Ensure is set to ''Absent''*' + } } } } From 0afbd3009561cf01c4f53cb0af48ef51d0654a8a Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 17:48:16 +0200 Subject: [PATCH 11/37] Clarify guideline on nullable property types in DSC class-based resources --- ...sc-community-style-guidelines-class-resource.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d1782663ea..3f721279d0 100644 --- a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md @@ -12,7 +12,7 @@ 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 -- Properties must have nullable types (`[Nullable[{Type}]]` where needed) +- Properties must have nullable types for types not nullable (`[Nullable[{Type}]]`) ## Required constructor From c4050a51c63d74a6789c9de6e6b00a3878de7901 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 17:54:11 +0200 Subject: [PATCH 12/37] Clarify guidelines for nullable property types in DSC class-based resources --- ...y-style-guidelines-class-resource.instructions.md | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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 3f721279d0..7ed592c762 100644 --- a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md @@ -12,7 +12,7 @@ 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 -- Properties must have nullable types for types not nullable (`[Nullable[{Type}]]`) +- value-type properties: Use `[Nullable[{FullTypeName}]]` (e.g., `[Nullable[System.Int32]]`) ## Required constructor @@ -24,16 +24,6 @@ MyResourceName () : base () } ``` -## Required constructor - -```powershell -MyResourceName () : base ($PSScriptRoot) -{ - # Property names where state cannot be enforced, e.g Ensure - $this.ExcludeDscProperties = @() -} -``` - ## Required Method Pattern ```powershell From 207ce9c635855b82e25dc7ad34d77d2af960e08d Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 18:18:35 +0200 Subject: [PATCH 13/37] Add support for MessageId in SqlAgentAlert configuration and tests --- .../DSC_SqlAgentAlert.Integration.Tests.ps1 | 11 ++- .../Resources/DSC_SqlAgentAlert.config.ps1 | 79 ++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.Integration.Tests.ps1 index e8c72d5ab4..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' { diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 index 5d3fc0a12e..f5070a7c02 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,57 @@ 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 + 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 = "SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033" + GetQuery = "SELECT message_id, text FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033" + + 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,11 +126,34 @@ 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 + 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 = "SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) THEN 1 ELSE 0 END AS MessageRemoved" + GetQuery = "SELECT COUNT(*) as MessageCount FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033" + + PsDscRunAsCredential = New-Object ` + -TypeName System.Management.Automation.PSCredential ` + -ArgumentList @($Node.Username, (ConvertTo-SecureString -String $Node.Password -AsPlainText -Force)) + + DependsOn = '[SqlAgentAlert]Integration_Test' + } } } From 5856af5cd5b9dd247a8437df2209abb4a4a39076 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 18:41:04 +0200 Subject: [PATCH 14/37] Update module name placeholders in integration, localization, and unit test guidelines --- ...mmunity-style-guidelines-integration-tests.instructions.md | 2 +- ...sc-community-style-guidelines-localization.instructions.md | 4 ++-- .../dsc-community-style-guidelines-unit-tests.instructions.md | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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..e8de17c3b7 100644 --- a/.github/instructions/dsc-community-style-guidelines-integration-tests.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-integration-tests.instructions.md @@ -45,7 +45,7 @@ BeforeDiscovery { } BeforeAll { - $script:dscModuleName = 'SqlServerDsc' + $script:dscModuleName = '{MyModuleName}' Import-Module -Name $script:dscModuleName -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-unit-tests.instructions.md b/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md index e798f97db6..a54f132475 100644 --- a/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-unit-tests.instructions.md @@ -42,7 +42,7 @@ BeforeDiscovery { } BeforeAll { - $script:dscModuleName = 'SqlServerDsc' + $script:dscModuleName = '{MyModuleName}' Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' From 1c747913c5132c7934563fa2a237636d346f1f09 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 18:44:14 +0200 Subject: [PATCH 15/37] Refactor AI instructions header to generalize project guidance --- .github/copilot-instructions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 342c43c855..aab305d39c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,7 +1,7 @@ -# 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. ## Instructions Overview From 670f85324c0c46627819cf040a979f5092c469fb Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 19:37:39 +0200 Subject: [PATCH 16/37] Clarify hashtable formatting guideline for multi-line properties --- .../dsc-community-style-guidelines-powershell.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md index dde0765dae..43b2297815 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 From 6d1226d1e01af640ee46dde82c84c2e5b79a2f6c Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 20:07:16 +0200 Subject: [PATCH 17/37] Add "RAISERROR" to cSpell words list in settings.json --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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" From d75a5da6d452306ebc53aa73aad5080612ad9f66 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 20:07:21 +0200 Subject: [PATCH 18/37] Enhance SQL Agent alert configurations to utilize MessageId with RAISERROR for better error handling --- .../Resources/DSC_SqlAgentAlert.config.ps1 | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 index f5070a7c02..828352fdd3 100644 --- a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 +++ b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 @@ -75,6 +75,7 @@ Configuration DSC_SqlAgentAlert_ChangeToMessageId_Config Id = 'CreateCustomMessage' InstanceName = $Node.InstanceName ServerName = $Node.ServerName + # cSpell: ignore addmessage msgnum msgtext SetQuery = " IF NOT EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) BEGIN @@ -85,8 +86,17 @@ Configuration DSC_SqlAgentAlert_ChangeToMessageId_Config @lang = 'us_english' END " - TestQuery = "SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033" - GetQuery = "SELECT message_id, text FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033" + 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 ` @@ -138,6 +148,7 @@ Configuration DSC_SqlAgentAlert_Remove_Config Id = 'RemoveCustomMessage' InstanceName = $Node.InstanceName ServerName = $Node.ServerName + # cSpell: ignore dropmessage SetQuery = " IF EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) BEGIN @@ -146,8 +157,17 @@ Configuration DSC_SqlAgentAlert_Remove_Config @lang = 'us_english' END " - TestQuery = "SELECT CASE WHEN NOT EXISTS (SELECT 1 FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033) THEN 1 ELSE 0 END AS MessageRemoved" - GetQuery = "SELECT COUNT(*) as MessageCount FROM sys.messages WHERE message_id = $($Node.MessageId) AND language_id = 1033" + 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 ` From 2d0a7da4fab9f6a9bad4b2519900c34788fa0f74 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 21:40:08 +0200 Subject: [PATCH 19/37] Add optional encryption parameter to SQL Agent alert configurations --- tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 index 828352fdd3..70f55fa441 100644 --- a/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 +++ b/tests/Integration/Resources/DSC_SqlAgentAlert.config.ps1 @@ -75,6 +75,7 @@ Configuration DSC_SqlAgentAlert_ChangeToMessageId_Config 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) @@ -148,6 +149,7 @@ Configuration DSC_SqlAgentAlert_Remove_Config 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) From 8a08ce462b090a62429ae18e738ea2af15d422b1 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 21:59:30 +0200 Subject: [PATCH 20/37] Enhance PowerShell guidelines with output stream usage and backtick line continuation rules --- ...ity-style-guidelines-powershell.instructions.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md index 43b2297815..ec193a1efc 100644 --- a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md @@ -79,13 +79,17 @@ 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 +- 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 -- 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. ## ShouldProcess Required Pattern From c4263f1182e97083932a33b87ea1699b5b587ab0 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Tue, 26 Aug 2025 22:20:06 +0200 Subject: [PATCH 21/37] Correct capitalization in PowerShell guidelines for Write-Verbose usage --- .../dsc-community-style-guidelines-powershell.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md index ec193a1efc..1b70f9451f 100644 --- a/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-powershell.instructions.md @@ -85,7 +85,7 @@ applyTo: "**/*.ps?(m|d)1" - 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-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 From 54a02bfc5422b72e138e75a0852928ce97689836 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Wed, 27 Aug 2025 07:29:37 +0200 Subject: [PATCH 22/37] Fix variable name in BeforeAll block for consistency in integration test guidelines --- ...mmunity-style-guidelines-integration-tests.instructions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e8de17c3b7..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 = '{MyModuleName}' + $script:moduleName = '{MyModuleName}' - Import-Module -Name $script:dscModuleName -Force -ErrorAction 'Stop' + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' } ``` From b3a7de8b43514ba4cb278bcc364eaa9ec8deacda Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Wed, 27 Aug 2025 07:30:23 +0200 Subject: [PATCH 23/37] Fix variable name in BeforeAll block for consistency in unit test guidelines --- ...unity-style-guidelines-unit-tests.instructions.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 a54f132475..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 = '{MyModuleName}' + $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 } ``` From 8fceb2a293924155b1f82a28066697b95ff01764 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Wed, 27 Aug 2025 13:12:25 +0200 Subject: [PATCH 24/37] Refactor naming section in Pester guidelines for clarity and consistency --- ...-community-style-guidelines-pester.instructions.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/instructions/dsc-community-style-guidelines-pester.instructions.md b/.github/instructions/dsc-community-style-guidelines-pester.instructions.md index 5e6e387abd..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,9 +31,6 @@ 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 From 809953f367ee903969a43e44a5b78e902ef46948 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 12:42:55 +0200 Subject: [PATCH 25/37] Enhance SqlAgentAlert validation logic in AssertProperties method and update tests for parameter checks --- source/Classes/020.SqlAgentAlert.ps1 | 41 ++++++-- tests/Unit/Classes/SqlAgentAlert.Tests.ps1 | 112 ++++++++++++++++++++- 2 files changed, 139 insertions(+), 14 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index 40bab81b90..e017845370 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -169,19 +169,42 @@ class SqlAgentAlert : SqlResourceBase hidden [void] AssertProperties([System.Collections.Hashtable] $properties) { - # TODO: Waiting for issue: https://github.com/dsccommunity/DscResource.Common/issues/160 - if ($properties.Ensure -eq 'Present') - { - # Validate that at least one of Severity or MessageId is specified - # TODO: Waiting for issue: https://github.com/dsccommunity/DscResource.Common/issues/161 - #Assert-BoundParameter -BoundParameterList $properties -AtLeastOneList @('Severity', 'MessageId') + # Validate that at least one of Severity or MessageId is specified + $assertAtLeastOneParams = @{ + BoundParameterList = $properties + AtLeastOneList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } + } - # Validate that both Severity and MessageId are not specified - Assert-BoundParameter -BoundParameterList $properties -MutuallyExclusiveList1 @('Severity') -MutuallyExclusiveList2 @('MessageId') + Assert-BoundParameter @assertAtLeastOneParams + + # Validate that both Severity and MessageId are not specified + $assertMutuallyExclusiveParams = @{ + BoundParameterList = $properties + MutuallyExclusiveList1 = @('Severity') + MutuallyExclusiveList2 = @('MessageId') + IfEqualParameterList = @{ + Ensure = 'Present' + } } - else + + Assert-BoundParameter @assertMutuallyExclusiveParams + + if ($properties.Ensure -eq 'Absent') { # When Ensure is 'Absent', Severity and MessageId must not be set + $assertAbsentParams = @{ + BoundParameterList = $properties + NotAllowedList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Absent' + } + } + + Assert-BoundParameter @assertAbsentParams + if ($properties.ContainsKey('Severity') -or $properties.ContainsKey('MessageId')) { $errorMessage = $this.localizedData.SqlAgentAlert_SeverityOrMessageIdNotAllowedWhenAbsent diff --git a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 index b854ddee84..41b4032ad2 100644 --- a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -145,6 +145,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { $instance = [SqlAgentAlert] @{ Name = 'TestAlert' InstanceName = 'MSSQLSERVER' + Severity = 17 } | Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { return $script:mockServerObject @@ -168,6 +169,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { $instance = [SqlAgentAlert] @{ Name = 'TestAlert' InstanceName = 'MSSQLSERVER' + Severity = 17 } | Add-Member -Force -MemberType 'ScriptMethod' -Name 'GetServerObject' -Value { return $script:mockServerObject @@ -340,14 +342,14 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } Context 'When Ensure is Present' { - It 'Should not throw when neither Severity nor MessageId are specified' { + It 'Should throw when neither Severity nor MessageId are specified' { InModuleScope -ScriptBlock { $properties = @{ Name = 'TestAlert' Ensure = 'Present' } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0052)*' } } @@ -406,7 +408,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Severity = 16 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*Cannot specify Severity or MessageId when Ensure is set to ''Absent''*' + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0053)*' } } @@ -418,7 +420,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { MessageId = 50001 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*Cannot specify Severity or MessageId when Ensure is set to ''Absent''*' + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0053)*' } } @@ -431,7 +433,107 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { MessageId = 50001 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*Cannot specify Severity or MessageId when Ensure is set to ''Absent''*' + { $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 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + + 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 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + + 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' + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -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 + } + } + + 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 } } } From 05fe912b6d26fd45d0a3a37156433e882de78076 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 17:51:20 +0200 Subject: [PATCH 26/37] Update SqlAgentAlert documentation and adjust validation ranges for Severity and MessageId parameters --- source/Classes/020.SqlAgentAlert.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index e017845370..bd2e6ee3ec 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -7,6 +7,9 @@ The `SqlAgentAlert` DSC resource is used to create, modify, or remove _SQL Server Agent_ alerts. + An alert can be switch between using system message and severity-based alerts by passing + the required parameter. It will switch the alert type 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 also possible to instead use impersonation by the @@ -125,12 +128,12 @@ class SqlAgentAlert : SqlResourceBase $Ensure = 'Present' [DscProperty()] - [ValidateRange(0, 25)] + [ValidateRange(1, 25)] [Nullable[System.Int32]] $Severity [DscProperty()] - [ValidateRange(0, 2147483647)] + [ValidateRange(1, 2147483647)] [Nullable[System.Int32]] $MessageId From cd6e952f3ad55e94785f96d0f9504bb429855dce Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 17:52:59 +0200 Subject: [PATCH 27/37] Update SqlAgentAlert parameter documentation to correct valid ranges for Severity and MessageId --- source/Classes/020.SqlAgentAlert.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index bd2e6ee3ec..0937156966 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -56,11 +56,11 @@ Default value is `'Present'`. .PARAMETER Severity - The severity of the _SQL Server Agent_ alert. Valid range is 0 to 25. + 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 0 to 2147483647. + The message id of the _SQL Server Agent_ alert. Valid range is 1 to 2147483647. Cannot be used together with **Severity**. .EXAMPLE From b60438e37855ba71f27c16b5e7169833bb8432cd Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 19:10:24 +0200 Subject: [PATCH 28/37] Fix constructor definition in DSC class guidelines to include $PSScriptRoot --- ...sc-community-style-guidelines-class-resource.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7ed592c762..abd5a3766a 100644 --- a/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md +++ b/.github/instructions/dsc-community-style-guidelines-class-resource.instructions.md @@ -17,7 +17,7 @@ applyTo: "source/[cC]lasses/**/*.ps1" ## Required constructor ```powershell -MyResourceName () : base () +MyResourceName () : base ($PSScriptRoot) { # Property names where state cannot be enforced, e.g. IsSingleInstance, Force $this.ExcludeDscProperties = @() From f89856650dd7efc4fea52b60cf7fcabe63059506 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 19:10:29 +0200 Subject: [PATCH 29/37] Update SqlServerDsc guidelines to clarify resource inheritance and constructor definition --- .github/instructions/SqlServerDsc-guidelines.instructions.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/instructions/SqlServerDsc-guidelines.instructions.md b/.github/instructions/SqlServerDsc-guidelines.instructions.md index d4a96f95d3..cc8ec878ae 100644 --- a/.github/instructions/SqlServerDsc-guidelines.instructions.md +++ b/.github/instructions/SqlServerDsc-guidelines.instructions.md @@ -11,8 +11,9 @@ applyTo: "**" ## Resources - Database Engine resources: inherit `SqlResourceBase` - - Inheriting `SqlResourceBase`; add `InstanceName`, `ServerName`, and `Credential` to `$this.ExcludeDscProperties` + - 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 From 51175ede7ec9cfbc94392a36aedb0ea0080aa559 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 19:10:34 +0200 Subject: [PATCH 30/37] Refine documentation for SqlAgentAlert to improve clarity on alert types and credential usage --- source/Classes/020.SqlAgentAlert.ps1 | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index 0937156966..ce9f1c2a11 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -7,13 +7,14 @@ The `SqlAgentAlert` DSC resource is used to create, modify, or remove _SQL Server Agent_ alerts. - An alert can be switch between using system message and severity-based alerts by passing - the required parameter. It will switch the alert type accordingly. + 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 also possible to instead use impersonation by the - parameter **Credential**. + instance as that user. It is also possible to use impersonation via the + **Credential** parameter. ## Requirements @@ -31,20 +32,20 @@ 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 using advanced type like the parameter - **Reasons** have. + together with class-based resources that use advanced types, such as the + **Reasons** parameter. ### Using **Credential** property - SQL Authentication and Group Managed Service Accounts is not supported as - impersonation credentials. Currently only Windows Integrated Security is - supported to use as credentials. + 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 non-domain - (for example a local Windows Server account) account the username must be - provided without the NetBIOS name, e.g. `username`. Using the NetBIOS name, e.g - using the format `DOMAIN\username` will not work. + 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). From 33540943def763f978570e4221426197160961fb Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 19:15:24 +0200 Subject: [PATCH 31/37] Refactor SqlAgentAlert to streamline absence validation for Severity and MessageId parameters --- source/Classes/020.SqlAgentAlert.ps1 | 24 ++++++------------------ source/en-US/SqlAgentAlert.strings.psd1 | 1 - 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index ce9f1c2a11..777d9269bb 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -196,24 +196,12 @@ class SqlAgentAlert : SqlResourceBase Assert-BoundParameter @assertMutuallyExclusiveParams - if ($properties.Ensure -eq 'Absent') - { - # When Ensure is 'Absent', Severity and MessageId must not be set - $assertAbsentParams = @{ - BoundParameterList = $properties - NotAllowedList = @('Severity', 'MessageId') - IfEqualParameterList = @{ - Ensure = 'Absent' - } - } - - Assert-BoundParameter @assertAbsentParams - - if ($properties.ContainsKey('Severity') -or $properties.ContainsKey('MessageId')) - { - $errorMessage = $this.localizedData.SqlAgentAlert_SeverityOrMessageIdNotAllowedWhenAbsent - - New-InvalidArgumentException -ArgumentName 'Severity, MessageId' -Message $errorMessage + # When Ensure is 'Absent', Severity and MessageId must not be set + $assertAbsentParams = @{ + BoundParameterList = $properties + NotAllowedList = @('Severity', 'MessageId') + IfEqualParameterList = @{ + Ensure = 'Absent' } } } diff --git a/source/en-US/SqlAgentAlert.strings.psd1 b/source/en-US/SqlAgentAlert.strings.psd1 index ef89513300..43da4e143d 100644 --- a/source/en-US/SqlAgentAlert.strings.psd1 +++ b/source/en-US/SqlAgentAlert.strings.psd1 @@ -12,6 +12,5 @@ ConvertFrom-StringData @' 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_SeverityOrMessageIdNotAllowedWhenAbsent = Cannot specify Severity or MessageId when Ensure is set to 'Absent'. (SAAA0008) SqlAgentAlert_NoChangesNeeded = No changes needed for SQL Agent Alert '{0}'. (SAAA0009) '@ From 5c97552f52156a3068f6e2b09187f559a2fc5f27 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 20:05:22 +0200 Subject: [PATCH 32/37] Fix formatting in changelog guidelines for issue references --- .../dsc-community-style-guidelines-changelog.instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 0a2fba5ab408f238ec2c08e7523cfbb9a52ad648 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Thu, 28 Aug 2025 20:06:58 +0200 Subject: [PATCH 33/37] Add assertion for bound parameters in SqlAgentAlert Set method --- source/Classes/020.SqlAgentAlert.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/Classes/020.SqlAgentAlert.ps1 b/source/Classes/020.SqlAgentAlert.ps1 index 777d9269bb..a447a8d659 100644 --- a/source/Classes/020.SqlAgentAlert.ps1 +++ b/source/Classes/020.SqlAgentAlert.ps1 @@ -204,6 +204,8 @@ class SqlAgentAlert : SqlResourceBase Ensure = 'Absent' } } + + Assert-BoundParameter @assertAbsentParams } hidden [System.Collections.Hashtable] GetCurrentState([System.Collections.Hashtable] $properties) From a245ba281858a4fde1c9517e55d024af5ea9e950 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Fri, 29 Aug 2025 15:45:56 +0200 Subject: [PATCH 34/37] Add tests for Modify() method in SqlAgentAlert to validate alert updates --- tests/Unit/Classes/SqlAgentAlert.Tests.ps1 | 122 +++++++++++++++++++++ 1 file changed, 122 insertions(+) diff --git a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 index 41b4032ad2..d63f1c18ec 100644 --- a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -311,6 +311,128 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } + Context 'When testing the hidden method Modify()' { + 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 + } + + { $instance.Modify($properties) } | Should -Not -Throw + + 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 + } + + { $instance.Modify($properties) } | Should -Not -Throw + + 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 + } + + { $instance.Modify($properties) } | Should -Not -Throw + + 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 passing mutually exclusive parameters' { Context 'When passing both Severity and MessageId' { BeforeAll { From 250b7f0b36fb94751d480a34b6a56bb8cf302558 Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Fri, 29 Aug 2025 16:05:47 +0200 Subject: [PATCH 35/37] Update build and test workflow instructions for clarity and consistency --- .github/copilot-instructions.md | 5 +++++ .../dsc-community-style-guidelines.instructions.md | 7 +++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index aab305d39c..f123e70475 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,6 +3,11 @@ 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 The guidelines always take priority over existing code patterns in project. 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` From 87fec7ebf30a2eb50a5e3574d86846e78b5bed9a Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Fri, 29 Aug 2025 16:06:03 +0200 Subject: [PATCH 36/37] Refactor test descriptions in SqlAgentAlert.Tests.ps1 for clarity and consistency --- tests/Unit/Classes/SqlAgentAlert.Tests.ps1 | 342 ++++++++++----------- 1 file changed, 171 insertions(+), 171 deletions(-) diff --git a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 index d63f1c18ec..79a1ff6cfb 100644 --- a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -53,7 +53,7 @@ AfterAll { } Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { - Context 'When class is instantiated' { + Context 'When using the constructor' { It 'Should not throw an exception' { InModuleScope -ScriptBlock { { [SqlAgentAlert]::new() } | Should -Not -Throw @@ -73,9 +73,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { $instance.GetType().Name | Should -Be 'SqlAgentAlert' } } - } - Context 'When setting and getting properties' { It 'Should be able to set and get the Name property' { InModuleScope -ScriptBlock { $instance = [SqlAgentAlert]::new() @@ -125,7 +123,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } - Context 'When testing Get() method' { + Context 'When using the Get() method' { BeforeAll { InModuleScope -ScriptBlock { $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' @@ -184,7 +182,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } - Context 'When testing Test() method' { + Context 'When using the Test() method' { BeforeAll { InModuleScope -ScriptBlock { $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' @@ -241,8 +239,8 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } - Context 'When testing Set() method' { - Context 'when it does not exist and Ensure is Present' { + 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' @@ -311,7 +309,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } - Context 'When testing the hidden method Modify()' { + Context 'When using the hidden Modify() method' { BeforeAll { InModuleScope -ScriptBlock { $script:mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server' @@ -433,229 +431,231 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { } } - Context 'When passing mutually exclusive parameters' { - Context 'When passing both Severity and MessageId' { - BeforeAll { - InModuleScope -ScriptBlock { - $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + 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 - } + 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*' + { $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 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' - } + 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)*' + { $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 + It 'Should not throw when only Severity is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } + + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw } + } + + It 'Should not throw when only MessageId is specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + MessageId = 50001 + } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + } } } - It 'Should not throw when only MessageId is specified' { - InModuleScope -ScriptBlock { - $properties = @{ - Name = 'TestAlert' - Ensure = 'Present' - MessageId = 50001 - } + Context 'When Ensure is Absent' { + It 'Should not throw when only Name and Ensure are specified' { + InModuleScope -ScriptBlock { + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + } } } } - Context 'When Ensure is Absent' { - It 'Should not throw when only Name and Ensure are specified' { + Context 'When passing invalid parameter combinations' { + BeforeAll { InModuleScope -ScriptBlock { - $properties = @{ - Name = 'TestAlert' - Ensure = 'Absent' - } - - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() } } - } - } - 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 + } - 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)*' } - - { $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 - } + 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)*' + { $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 - } + 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)*' + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw -ExpectedMessage '*(DRC0053)*' + } } } } - } - Context 'When validating Assert-BoundParameter calls' { - BeforeAll { - InModuleScope -ScriptBlock { - $script:mockSqlAgentAlertInstance = [SqlAgentAlert]::new() + 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 { } + 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 - } + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw - 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 + 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 { } + 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 - } + $properties = @{ + Name = 'TestAlert' + Ensure = 'Present' + Severity = 16 + } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw - 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 + 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 { } + 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' - } + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -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 + 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' + 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 - } + $properties = @{ + Name = 'TestAlert' + Ensure = 'Absent' + Severity = 16 + } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Throw + { $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 + 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 + } } } } From 2f86af9928d4bc79e47227c9205f4e0185a5777d Mon Sep 17 00:00:00 2001 From: Johan Ljunggren Date: Fri, 29 Aug 2025 16:30:26 +0200 Subject: [PATCH 37/37] Refactor test assertions in SqlAgentAlert.Tests.ps1 to use assignment for clarity --- tests/Unit/Classes/SqlAgentAlert.Tests.ps1 | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 index 79a1ff6cfb..b170746de0 100644 --- a/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 +++ b/tests/Unit/Classes/SqlAgentAlert.Tests.ps1 @@ -56,7 +56,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Context 'When using the constructor' { It 'Should not throw an exception' { InModuleScope -ScriptBlock { - { [SqlAgentAlert]::new() } | Should -Not -Throw + $null = [SqlAgentAlert]::new() } } @@ -263,7 +263,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { return $script:mockServerObject } -PassThru - { $instance.Set() } | Should -Not -Throw + $null = $instance.Set() Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 1 -Scope It Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 0 -Scope It @@ -300,7 +300,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { return $script:mockServerObject } -PassThru - { $instance.Set() } | Should -Not -Throw + $null = $instance.Set() Should -Invoke -CommandName 'New-SqlDscAgentAlert' -Exactly -Times 0 -Scope It Should -Invoke -CommandName 'Remove-SqlDscAgentAlert' -Exactly -Times 1 -Scope It @@ -346,7 +346,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Severity = 16 } - { $instance.Modify($properties) } | Should -Not -Throw + $null = $instance.Modify($properties) Should -Invoke -CommandName 'Get-SqlDscAgentAlert' -Exactly -Times 1 -Scope It Should -Invoke -CommandName 'Set-SqlDscAgentAlert' -ParameterFilter { @@ -383,7 +383,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { MessageId = 50002 } - { $instance.Modify($properties) } | Should -Not -Throw + $null = $instance.Modify($properties) Should -Invoke -CommandName 'Get-SqlDscAgentAlert' -Exactly -Times 1 -Scope It Should -Invoke -CommandName 'Set-SqlDscAgentAlert' -ParameterFilter { @@ -420,7 +420,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Severity = 16 } - { $instance.Modify($properties) } | Should -Not -Throw + $null = $instance.Modify($properties) Should -Invoke -CommandName 'Get-SqlDscAgentAlert' -Exactly -Times 1 -Scope It Should -Invoke -CommandName 'Set-SqlDscAgentAlert' -Exactly -Times 0 -Scope It @@ -482,7 +482,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Severity = 16 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) } } @@ -494,7 +494,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { MessageId = 50001 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) } } } @@ -507,7 +507,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Ensure = 'Absent' } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) } } } @@ -578,7 +578,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Severity = 16 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { $BoundParameterList -is [hashtable] -and @@ -599,7 +599,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Severity = 16 } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { $BoundParameterList -is [hashtable] -and @@ -621,7 +621,7 @@ Describe 'SqlAgentAlert' -Tag 'SqlAgentAlert' { Ensure = 'Absent' } - { $script:mockSqlAgentAlertInstance.AssertProperties($properties) } | Should -Not -Throw + $null = $script:mockSqlAgentAlertInstance.AssertProperties($properties) Should -Invoke -CommandName 'Assert-BoundParameter' -ParameterFilter { $BoundParameterList -is [hashtable] -and