Skip to content

Commit 9305e16

Browse files
authored
Add Assert-SqlDscLogin command to validate SQL Server login existence (#2123)
1 parent 79dcc9f commit 9305e16

File tree

9 files changed

+456
-2
lines changed

9 files changed

+456
-2
lines changed

.github/copilot-instructions.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ PowerShell commands that should be public should always have its separate
1010
script file and the command name as the file name with the .ps1 extension,
1111
these files shall always be placed in the folder source/Public.
1212

13+
All public command names must have the noun prefixed with 'SqlDsc', e.g.
14+
{Verb}-SqlDsc{Noun}.
15+
1316
Public commands may use private functions to move out logic that can be
1417
reused by other public commands, so move out any logic that can be deemed
1518
reusable.
@@ -196,6 +199,10 @@ case (`It`-block) as possible.
196199
Never test, mock or use `Should -Invoke` for `Write-Verbose` and `Write-Debug`
197200
regardless of other instructions.
198201

202+
Never use `Should -Not -Throw` to prepare for Pester v6 where it has been
203+
removed. By default the `It` block will handle any unexpected exception.
204+
Instead of `{ Command } | Should -Not -Throw`, use `Command` directly.
205+
199206
Unit tests should be added for all public commands, private functions and
200207
class-based resources. The unit tests for class-based resources should be
201208
placed in the folder tests/Unit/Classes. The unit tests for public command
@@ -206,6 +213,83 @@ they are testing, but should have the suffix .Tests.ps1. The unit tests
206213
should be written to cover all possible scenarios and code paths, ensuring
207214
that both edge cases and common use cases are tested.
208215

216+
All public commands should always have a test to validate parameter sets
217+
using this template. For commands with a single parameter set:
218+
219+
```powershell
220+
It 'Should have the correct parameters in parameter set <MockParameterSetName>' -ForEach @(
221+
@{
222+
MockParameterSetName = '__AllParameterSets'
223+
MockExpectedParameters = '[-Parameter1] <Type> [-Parameter2] <Type> [<CommonParameters>]'
224+
}
225+
) {
226+
$result = (Get-Command -Name 'CommandName').ParameterSets |
227+
Where-Object -FilterScript {
228+
$_.Name -eq $mockParameterSetName
229+
} |
230+
Select-Object -Property @(
231+
@{
232+
Name = 'ParameterSetName'
233+
Expression = { $_.Name }
234+
},
235+
@{
236+
Name = 'ParameterListAsString'
237+
Expression = { $_.ToString() }
238+
}
239+
)
240+
241+
$result.ParameterSetName | Should -Be $MockParameterSetName
242+
$result.ParameterListAsString | Should -Be $MockExpectedParameters
243+
}
244+
```
245+
246+
For commands with multiple parameter sets, use this pattern:
247+
248+
```powershell
249+
It 'Should have the correct parameters in parameter set <MockParameterSetName>' -ForEach @(
250+
@{
251+
MockParameterSetName = 'ParameterSet1'
252+
MockExpectedParameters = '-ServerObject <Server> -Name <string> -Parameter1 <string> [<CommonParameters>]'
253+
}
254+
@{
255+
MockParameterSetName = 'ParameterSet2'
256+
MockExpectedParameters = '-ServerObject <Server> -Name <string> -Parameter2 <uint> [<CommonParameters>]'
257+
}
258+
) {
259+
$result = (Get-Command -Name 'CommandName').ParameterSets |
260+
Where-Object -FilterScript {
261+
$_.Name -eq $mockParameterSetName
262+
} |
263+
Select-Object -Property @(
264+
@{
265+
Name = 'ParameterSetName'
266+
Expression = { $_.Name }
267+
},
268+
@{
269+
Name = 'ParameterListAsString'
270+
Expression = { $_.ToString() }
271+
}
272+
)
273+
274+
$result.ParameterSetName | Should -Be $MockParameterSetName
275+
$result.ParameterListAsString | Should -Be $MockExpectedParameters
276+
}
277+
```
278+
279+
All public commands should also include tests to validate parameter properties:
280+
281+
```powershell
282+
It 'Should have ParameterName as a mandatory parameter' {
283+
$parameterInfo = (Get-Command -Name 'CommandName').Parameters['ParameterName']
284+
$parameterInfo.Attributes.Mandatory | Should -Contain $true
285+
}
286+
287+
It 'Should accept ParameterName from pipeline' {
288+
$parameterInfo = (Get-Command -Name 'CommandName').Parameters['ParameterName']
289+
$parameterInfo.Attributes.ValueFromPipeline | Should -Contain $true
290+
}
291+
```
292+
209293
The `BeforeAll` block should be used to set up any necessary test data or mocking
210294

211295
Use localized strings in the tests only when necessary. You can assign the
@@ -302,6 +386,19 @@ edge cases and common use cases are tested. The integration tests should
302386
also be written to test the command in a real environment, using real
303387
resources and dependencies.
304388

389+
Integration test script files for public commands must be added to a group
390+
within the 'Integration_Test_Commands_SqlServer' stage in ./azure-pipelines.yml.
391+
Choose the appropriate group number based on the dependencies of the command
392+
being tested (e.g., commands that require Database Engine should be in Group 2
393+
or later, after the Database Engine installation tests).
394+
395+
When integration tests need the computer name in CI environments, always use
396+
the Get-ComputerName command, which is available in the build pipeline.
397+
398+
For integration testing commands use the information in the
399+
tests/Integration/Commands/README.md, which describes the testing environment
400+
including available instances, users, credentials, and other configuration details.
401+
305402
All integration tests must use the below code block prior to the first
306403
`Describe`-block. The following code will set up the integration test
307404
environment and it will make sure the module being tested is available
@@ -343,6 +440,24 @@ The module DscResource.Test is used by the pipeline and its commands
343440
are normally not used when testing public functions, private functions or
344441
class-based resources.
345442

443+
## SQL Server
444+
445+
### SQL Server Management Objects (SMO)
446+
447+
When developing commands, private functions, class-based resources, or making
448+
modifications to existing functionality, always prefer using SQL Server
449+
Management Objects (SMO) as the primary method for interacting with SQL Server.
450+
Only use T-SQL when it is not possible to achieve the desired functionality
451+
with SMO.
452+
453+
## Change log
454+
455+
The Unreleased section in CHANGELOG.md should always be updated when making
456+
changes to the codebase. Use the keepachangelog format and provide concrete
457+
release notes that describe the main changes made. This includes new commands,
458+
private functions, class-based resources, or significant modifications to
459+
existing functionality.
460+
346461
## Style guidelines
347462

348463
This project use the style guidelines from the DSC Community: https://dsccommunity.org/styleguidelines
@@ -356,6 +471,7 @@ This project use the style guidelines from the DSC Community: https://dsccommuni
356471
### PowerShell files
357472

358473
- All files should use UTF8 without BOM.
474+
- All files must end with a new line.
359475

360476
### PowerShell code
361477

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
"analyzersettings",
7878
"sqlcmd",
7979
"PBIRS",
80-
"SSRS"
80+
"SSRS",
81+
"DSCSQLTEST"
8182
],
8283
"cSpell.ignorePaths": [
8384
".git"

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Added setup workflow for GitHub Copilot.
1111
- Switch the workflow to use Linux.
1212
- Attempt to unshallow the Copilot branch
13+
- `Assert-SqlDscLogin`
14+
- Added new public command to validate that a specified SQL Server principal
15+
exists as a login, throwing a terminating error if it doesn't exist.
16+
- Supports pipeline input and provides detailed error messages with localization.
17+
- Uses `Test-SqlDscIsLogin` command for login validation following module patterns.
1318

1419
### Changed
1520

1621
- `azure-pipelines.yml`
1722
- Remove `windows-2019` images fixes [#2106](https://github.com/dsccommunity/SqlServerDsc/issues/2106).
1823
- Move individual tasks to `windows-latest`.
24+
- Added integration tests for `Assert-SqlDscLogin` command in Group 2.
1925

2026
## [17.1.0] - 2025-05-22
2127

azure-pipelines.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ stages:
247247
# Group 1
248248
'tests/Integration/Commands/Install-SqlDscServer.Integration.Tests.ps1'
249249
'tests/Integration/Commands/Connect-SqlDscDatabaseEngine.Integration.Tests.ps1'
250+
# Group 2
251+
'tests/Integration/Commands/Assert-SqlDscLogin.Integration.Tests.ps1'
250252
# Group 9
251253
'tests/Integration/Commands/Uninstall-SqlDscServer.Integration.Tests.ps1'
252254
)
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<#
2+
.SYNOPSIS
3+
Assert that the specified SQL Server principal exists as a login.
4+
5+
.DESCRIPTION
6+
This command asserts that the specified SQL Server principal exists as a
7+
login. If the principal does not exist as a login, a terminating error
8+
is thrown.
9+
10+
.PARAMETER ServerObject
11+
Specifies current server connection object.
12+
13+
.PARAMETER Name
14+
Specifies the name of the principal that needs to exist as a login.
15+
16+
.EXAMPLE
17+
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
18+
$serverObject | Assert-SqlDscLogin -Name 'MyLogin'
19+
20+
Asserts that the principal 'MyLogin' exists as a login.
21+
22+
.EXAMPLE
23+
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
24+
Assert-SqlDscLogin -ServerObject $serverObject -Name 'MyLogin'
25+
26+
Asserts that the principal 'MyLogin' exists as a login.
27+
28+
.NOTES
29+
This command throws a terminating error if the specified SQL Server
30+
principal does not exist as a SQL server login.
31+
#>
32+
function Assert-SqlDscLogin
33+
{
34+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the rule does not yet support parsing the code when a parameter type is not available. The ScriptAnalyzer rule UseSyntacticallyCorrectExamples will always error in the editor due to https://github.com/indented-automation/Indented.ScriptAnalyzerRules/issues/8.')]
35+
[CmdletBinding()]
36+
param
37+
(
38+
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
39+
[Microsoft.SqlServer.Management.Smo.Server]
40+
$ServerObject,
41+
42+
[Parameter(Mandatory = $true)]
43+
[System.String]
44+
$Name
45+
)
46+
47+
process
48+
{
49+
Write-Verbose -Message ($script:localizedData.Assert_Login_CheckingLogin -f $Name, $ServerObject.InstanceName)
50+
51+
if (-not (Test-SqlDscIsLogin -ServerObject $ServerObject -Name $Name))
52+
{
53+
$missingLoginMessage = $script:localizedData.Assert_Login_LoginMissing -f $Name, $ServerObject.InstanceName
54+
55+
$PSCmdlet.ThrowTerminatingError(
56+
[System.Management.Automation.ErrorRecord]::new(
57+
$missingLoginMessage,
58+
'ASDL0001', # cspell: disable-line
59+
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
60+
$Name
61+
)
62+
)
63+
}
64+
65+
Write-Debug -Message ($script:localizedData.Assert_Login_LoginExists -f $Name)
66+
}
67+
}

source/en-US/SqlServerDsc.strings.psd1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,9 @@ ConvertFrom-StringData @'
222222
## ConvertTo-SqlDscEditionName
223223
ConvertTo_EditionName_ConvertingEditionId = Converting EditionId '{0}' to Edition name.
224224
ConvertTo_EditionName_UnknownEditionId = The EditionId '{0}' is unknown and could not be converted.
225+
226+
## Assert-SqlDscLogin
227+
Assert_Login_CheckingLogin = Checking if the principal '{0}' exists as a login on the instance '{1}'.
228+
Assert_Login_LoginMissing = The principal '{0}' does not exist as a login on the instance '{1}'.
229+
Assert_Login_LoginExists = The principal '{0}' exists as a login.
225230
'@
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Suppressing this rule because Script Analyzer does not understand Pester syntax.')]
2+
param ()
3+
4+
BeforeDiscovery {
5+
try
6+
{
7+
if (-not (Get-Module -Name 'DscResource.Test'))
8+
{
9+
# Assumes dependencies has been resolved, so if this module is not available, run 'noop' task.
10+
if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable))
11+
{
12+
# Redirect all streams to $null, except the error stream (stream 2)
13+
& "$PSScriptRoot/../../../build.ps1" -Tasks 'noop' 3>&1 4>&1 5>&1 6>&1 > $null
14+
}
15+
16+
# If the dependencies has not been resolved, this will throw an error.
17+
Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop'
18+
}
19+
}
20+
catch [System.IO.FileNotFoundException]
21+
{
22+
throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.'
23+
}
24+
}
25+
26+
BeforeAll {
27+
$script:dscModuleName = 'SqlServerDsc'
28+
29+
Import-Module -Name $script:dscModuleName
30+
}
31+
32+
Describe 'Assert-SqlDscLogin' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') {
33+
BeforeAll {
34+
# Starting the named instance SQL Server service prior to running tests.
35+
Start-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop'
36+
37+
$script:instanceName = 'DSCSQLTEST'
38+
$script:computerName = Get-ComputerName
39+
}
40+
41+
AfterAll {
42+
# Stop the named instance SQL Server service to save memory on the build worker.
43+
Stop-Service -Name 'MSSQL$DSCSQLTEST' -Verbose -ErrorAction 'Stop'
44+
}
45+
46+
Context 'When connecting to SQL Server instance' {
47+
BeforeAll {
48+
$sqlAdministratorUserName = 'SqlAdmin' # Using computer name as NetBIOS name throw exception.
49+
$sqlAdministratorPassword = ConvertTo-SecureString -String 'P@ssw0rd1' -AsPlainText -Force
50+
51+
$script:sqlAdminCredential = [System.Management.Automation.PSCredential]::new($sqlAdministratorUserName, $sqlAdministratorPassword)
52+
53+
$script:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:instanceName -Credential $script:sqlAdminCredential
54+
}
55+
56+
Context 'When a login exists' {
57+
It 'Should not throw an error for sa login' {
58+
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'sa' } | Should -Not -Throw
59+
}
60+
61+
It 'Should not throw an error when using pipeline' {
62+
{ $script:serverObject | Assert-SqlDscLogin -Name 'sa' } | Should -Not -Throw
63+
}
64+
65+
It 'Should not throw an error for NT AUTHORITY\SYSTEM login' {
66+
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'NT AUTHORITY\SYSTEM' } | Should -Not -Throw
67+
}
68+
69+
It 'Should not throw an error for SqlAdmin login' {
70+
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name ('{0}\SqlAdmin' -f $script:computerName) } | Should -Not -Throw
71+
}
72+
}
73+
74+
Context 'When a login does not exist' {
75+
It 'Should throw a terminating error for non-existent login' {
76+
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'NonExistentLogin123' } | Should -Throw -ExpectedMessage "*does not exist as a login*"
77+
}
78+
79+
It 'Should throw an error with ObjectNotFound category' {
80+
try
81+
{
82+
Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'NonExistentLogin123'
83+
}
84+
catch
85+
{
86+
$_.CategoryInfo.Category | Should -Be 'ObjectNotFound'
87+
}
88+
}
89+
}
90+
}
91+
}

tests/Integration/Commands/Uninstall-SqlDscServer.Integration.Tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ BeforeDiscovery {
2424
}
2525

2626
# cSpell: ignore DSCSQLTEST
27-
Describe 'Install-SqlDscServer' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') {
27+
Describe 'Uninstall-SqlDscServer' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') {
2828
BeforeAll {
2929
Write-Verbose -Message ('Running integration test as user ''{0}''.' -f $env:UserName) -Verbose
3030

0 commit comments

Comments
 (0)