Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2571727
Initial plan
Copilot Aug 1, 2025
29c116a
Add Assert-SqlLogin command with unit and integration tests
Copilot Aug 1, 2025
cf8760e
Update unit and integration tests to follow existing patterns
Copilot Aug 1, 2025
21ec499
Final implementation of Assert-SqlLogin command with validated tests
Copilot Aug 1, 2025
3ac3109
Merge branch 'main' into copilot/fix-11455cec-d078-4daf-820b-08aaf88b…
johlju Aug 2, 2025
3e5d2b5
Add Assert-SqlLogin integration test to CI pipeline and include SqlAd…
Copilot Aug 2, 2025
aab9a74
Fix missing newlines in Assert-SqlLogin files and update copilot inst…
Copilot Aug 2, 2025
f12f442
Update CHANGELOG.md with Assert-SqlLogin release notes and add change…
Copilot Aug 2, 2025
c4defdf
Apply suggestion from @Copilot
johlju Aug 2, 2025
43991fa
Improve copilot instructions for better AI understanding and add SQL …
Copilot Aug 2, 2025
59d8ba4
Fix FullyQualifiedErrorId test expectation to match PowerShell's erro…
Copilot Aug 2, 2025
8ac5882
Fix integration test authentication and add README reference to copil…
Copilot Aug 2, 2025
519de24
Rename Assert-SqlLogin to Assert-SqlDscLogin with Name parameter and …
Copilot Aug 2, 2025
b1fb4dc
Fix localized string key names and add missing newlines
Copilot Aug 2, 2025
c0e5b4a
Fix SqlAdmin credential format to use computer name prefix for local …
Copilot Aug 2, 2025
a4ddc3e
Refactor Assert-SqlDscLogin tests to manage SQL Server service lifecy…
johlju Aug 2, 2025
86b4e1c
Add "DSCSQLTEST" to cSpell words in settings.json
johlju Aug 2, 2025
90470e8
Rename test suite from 'Install-SqlDscServer' to 'Uninstall-SqlDscSer…
johlju Aug 2, 2025
fa0ca66
Update Assert-SqlDscLogin tests to start and stop the named instance …
johlju Aug 2, 2025
7d6f7ba
Update Assert-SqlDscLogin tests for Pester v6 compatibility and add p…
Copilot Aug 3, 2025
f3a9647
Fix unit tests for Pester v6 compatibility and improve copilot instru…
Copilot Aug 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ PowerShell commands that should be public should always have its separate
script file and the command name as the file name with the .ps1 extension,
these files shall always be placed in the folder source/Public.

All public command names must have the noun prefixed with 'SqlDsc', e.g.
{Verb}-SqlDsc{Noun}.

Public commands may use private functions to move out logic that can be
reused by other public commands, so move out any logic that can be deemed
reusable.
Expand Down Expand Up @@ -302,6 +305,19 @@ edge cases and common use cases are tested. The integration tests should
also be written to test the command in a real environment, using real
resources and dependencies.

Integration test script files for public commands must be added to a group
within the 'Integration_Test_Commands_SqlServer' stage in ./azure-pipelines.yml.
Choose the appropriate group number based on the dependencies of the command
being tested (e.g., commands that require Database Engine should be in Group 2
or later, after the Database Engine installation tests).

When integration tests need the computer name in CI environments, always use
the Get-ComputerName command, which is available in the build pipeline.

For integration testing commands use the information in the
tests/Integration/Commands/README.md, which describes the testing environment
including available instances, users, credentials, and other configuration details.

All integration tests must use the below code block prior to the first
`Describe`-block. The following code will set up the integration test
environment and it will make sure the module being tested is available
Expand Down Expand Up @@ -343,6 +359,24 @@ The module DscResource.Test is used by the pipeline and its commands
are normally not used when testing public functions, private functions or
class-based resources.

## SQL Server

### SQL Server Management Objects (SMO)

When developing commands, private functions, class-based resources, or making
modifications to existing functionality, always prefer using SQL Server
Management Objects (SMO) as the primary method for interacting with SQL Server.
Only use T-SQL when it is not possible to achieve the desired functionality
with SMO.

## Change log

The Unreleased section in CHANGELOG.md should always be updated when making
changes to the codebase. Use the keepachangelog format and provide concrete
release notes that describe the main changes made. This includes new commands,
private functions, class-based resources, or significant modifications to
existing functionality.

## Style guidelines

This project use the style guidelines from the DSC Community: https://dsccommunity.org/styleguidelines
Expand All @@ -356,6 +390,7 @@ This project use the style guidelines from the DSC Community: https://dsccommuni
### PowerShell files

- All files should use UTF8 without BOM.
- All files must end with a new line.

### PowerShell code

Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ 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
- `Assert-SqlDscLogin`
- Added new public command to validate that a specified SQL Server principal
exists as a login, throwing a terminating error if it doesn't exist.
- Supports pipeline input and provides detailed error messages with localization.
- Uses `Test-SqlDscIsLogin` command for login validation following module patterns.

### Changed

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

## [17.1.0] - 2025-05-22

Expand Down
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ stages:
# Group 1
'tests/Integration/Commands/Install-SqlDscServer.Integration.Tests.ps1'
'tests/Integration/Commands/Connect-SqlDscDatabaseEngine.Integration.Tests.ps1'
# Group 2
'tests/Integration/Commands/Assert-SqlDscLogin.Integration.Tests.ps1'
# Group 9
'tests/Integration/Commands/Uninstall-SqlDscServer.Integration.Tests.ps1'
)
Expand Down
67 changes: 67 additions & 0 deletions source/Public/Assert-SqlDscLogin.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<#
.SYNOPSIS
Assert that the specified SQL Server principal exists as a login.

.DESCRIPTION
This command asserts that the specified SQL Server principal exists as a
login. If the principal does not exist as a login, a terminating error
is thrown.

.PARAMETER ServerObject
Specifies current server connection object.

.PARAMETER Name
Specifies the name of the principal that needs to exist as a login.

.EXAMPLE
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
$serverObject | Assert-SqlDscLogin -Name 'MyLogin'

Asserts that the principal 'MyLogin' exists as a login.

.EXAMPLE
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
Assert-SqlDscLogin -ServerObject $serverObject -Name 'MyLogin'

Asserts that the principal 'MyLogin' exists as a login.

.NOTES
This command throws a terminating error if the specified SQL Server
principal does not exist as a SQL server login.
#>
function Assert-SqlDscLogin
{
[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.')]
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[Microsoft.SqlServer.Management.Smo.Server]
$ServerObject,

[Parameter(Mandatory = $true)]
[System.String]
$Name
)

process
{
Write-Verbose -Message ($script:localizedData.Assert_Login_CheckingLogin -f $Name, $ServerObject.InstanceName)

if (-not (Test-SqlDscIsLogin -ServerObject $ServerObject -Name $Name))
{
$missingLoginMessage = $script:localizedData.Assert_Login_LoginMissing -f $Name, $ServerObject.InstanceName

$PSCmdlet.ThrowTerminatingError(
[System.Management.Automation.ErrorRecord]::new(
$missingLoginMessage,
'ASDL0001', # cspell: disable-line
[System.Management.Automation.ErrorCategory]::ObjectNotFound,
$Name
)
)
}

Write-Debug -Message ($script:localizedData.Assert_Login_LoginExists -f $Name)
}
}
5 changes: 5 additions & 0 deletions source/en-US/SqlServerDsc.strings.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,9 @@ ConvertFrom-StringData @'
## ConvertTo-SqlDscEditionName
ConvertTo_EditionName_ConvertingEditionId = Converting EditionId '{0}' to Edition name.
ConvertTo_EditionName_UnknownEditionId = The EditionId '{0}' is unknown and could not be converted.

## Assert-SqlDscLogin
Assert_Login_CheckingLogin = Checking if the principal '{0}' exists as a login on the instance '{1}'.
Assert_Login_LoginMissing = The principal '{0}' does not exist as a login on the instance '{1}'.
Assert_Login_LoginExists = The principal '{0}' exists as a login.
'@
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
[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 has 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 has 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'

Import-Module -Name $script:dscModuleName
}

Describe 'Assert-SqlDscLogin' -Tag @('Integration_SQL2016', 'Integration_SQL2017', 'Integration_SQL2019', 'Integration_SQL2022') {
BeforeAll {
$script:instanceName = 'DSCSQLTEST'
$script:computerName = Get-ComputerName
}

Context 'When connecting to SQL Server instance' {
BeforeAll {
$script:sqlAdminCredential = New-Object -TypeName 'System.Management.Automation.PSCredential' -ArgumentList @(
('{0}\SqlAdmin' -f (Get-ComputerName)),
('P@ssw0rd1' | ConvertTo-SecureString -AsPlainText -Force)
)
$script:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:instanceName -Credential $script:sqlAdminCredential
}

Context 'When a login exists' {
It 'Should not throw an error for sa login' {
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'sa' } | Should -Not -Throw
}

It 'Should not throw an error when using pipeline' {
{ $script:serverObject | Assert-SqlDscLogin -Name 'sa' } | Should -Not -Throw
}

It 'Should not throw an error for NT AUTHORITY\SYSTEM login' {
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'NT AUTHORITY\SYSTEM' } | Should -Not -Throw
}

It 'Should not throw an error for SqlAdmin login' {
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name ('{0}\SqlAdmin' -f $script:computerName) } | Should -Not -Throw
}
}

Context 'When a login does not exist' {
It 'Should throw a terminating error for non-existent login' {
{ Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'NonExistentLogin123' } | Should -Throw -ExpectedMessage "*does not exist as a login*"
}

It 'Should throw an error with ObjectNotFound category' {
try
{
Assert-SqlDscLogin -ServerObject $script:serverObject -Name 'NonExistentLogin123'
}
catch
{
$_.CategoryInfo.Category | Should -Be 'ObjectNotFound'
}
}
}
}
}
146 changes: 146 additions & 0 deletions tests/Unit/Public/Assert-SqlDscLogin.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
param ()

BeforeDiscovery {
try
{
if (-not (Get-Module -Name 'DscResource.Test'))
{
# Assumes dependencies has 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 has 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'

$env:SqlServerDscCI = $true

Import-Module -Name $script:dscModuleName

# Loading mocked classes
Add-Type -Path (Join-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '../Stubs') -ChildPath 'SMO.cs')

$PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:dscModuleName
$PSDefaultParameterValues['Mock:ModuleName'] = $script:dscModuleName
$PSDefaultParameterValues['Should:ModuleName'] = $script:dscModuleName
}

AfterAll {
$PSDefaultParameterValues.Remove('InModuleScope:ModuleName')
$PSDefaultParameterValues.Remove('Mock:ModuleName')
$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

Remove-Item -Path 'env:SqlServerDscCI'
}

Describe 'Assert-SqlDscLogin' -Tag 'Public' {
Context 'When a login exists' {
BeforeAll {
$mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'
$mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force

Mock -CommandName 'Test-SqlDscIsLogin' -MockWith { return $true }
}

It 'Should not throw an error when the login exists' {
{ Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'TestLogin' } | Should -Not -Throw
}

It 'Should call Test-SqlDscIsLogin with correct parameters' {
Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'TestLogin'

Should -Invoke -CommandName 'Test-SqlDscIsLogin' -ParameterFilter {
$ServerObject.InstanceName -eq 'TestInstance' -and
$Name -eq 'TestLogin'
} -Exactly -Times 1
}

It 'Should accept ServerObject from pipeline' {
{ $mockServerObject | Assert-SqlDscLogin -Name 'TestLogin' } | Should -Not -Throw
}
}

Context 'When a login does not exist' {
BeforeAll {
$mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'
$mockServerObject | Add-Member -MemberType 'NoteProperty' -Name 'InstanceName' -Value 'TestInstance' -Force

Mock -CommandName 'Test-SqlDscIsLogin' -MockWith { return $false }
}

It 'Should throw a terminating error when the login does not exist' {
{ Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'NonExistentLogin' } | Should -Throw -ExpectedMessage "*does not exist as a login*"
}

It 'Should throw an error with the correct error category' {
try
{
Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'NonExistentLogin'
}
catch
{
$_.CategoryInfo.Category | Should -Be 'ObjectNotFound'
$_.FullyQualifiedErrorId | Should -Be 'ASDL0001,Assert-SqlDscLogin'
}
}

It 'Should include the principal name in the error message' {
{ Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'NonExistentLogin' } | Should -Throw -ExpectedMessage "*NonExistentLogin*"
}

It 'Should include the instance name in the error message' {
{ Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'NonExistentLogin' } | Should -Throw -ExpectedMessage "*TestInstance*"
}

It 'Should call Test-SqlDscIsLogin with correct parameters' {
try {
Assert-SqlDscLogin -ServerObject $mockServerObject -Name 'NonExistentLogin'
}
catch {
# Expected error
}

Should -Invoke -CommandName 'Test-SqlDscIsLogin' -ParameterFilter {
$ServerObject.InstanceName -eq 'TestInstance' -and
$Name -eq 'NonExistentLogin'
} -Exactly -Times 1
}
}

Context 'When validating parameters' {
BeforeAll {
$mockServerObject = New-Object -TypeName 'Microsoft.SqlServer.Management.Smo.Server'
}

It 'Should have ServerObject as a mandatory parameter' {
$parameterInfo = (Get-Command -Name 'Assert-SqlDscLogin').Parameters['ServerObject']
$parameterInfo.Attributes.Mandatory | Should -Contain $true
}

It 'Should have Name as a mandatory parameter' {
$parameterInfo = (Get-Command -Name 'Assert-SqlDscLogin').Parameters['Name']
$parameterInfo.Attributes.Mandatory | Should -Contain $true
}

It 'Should accept ServerObject from pipeline' {
$parameterInfo = (Get-Command -Name 'Assert-SqlDscLogin').Parameters['ServerObject']
$parameterInfo.Attributes.ValueFromPipeline | Should -Contain $true
}
}
}
Loading