Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
9 changes: 9 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,14 @@ 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.

Any integration tests script files for commands should be added to a group
in the stage 'Integration_Test_Commands_SqlServer' in ./azure-pipelines.yml.
The group number should be determined by what other command's integration test
it is dependent on.

To dynamically get the computer name the CI has, always use the command
Get-ComputerName (a command that is available in the build pipeline).

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 @@ -356,6 +364,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
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-SqlLogin.Integration.Tests.ps1'
# Group 9
'tests/Integration/Commands/Uninstall-SqlDscServer.Integration.Tests.ps1'
)
Expand Down
67 changes: 67 additions & 0 deletions source/Public/Assert-SqlLogin.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 Principal
Specifies the principal that need to exist as a login.

.EXAMPLE
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
$serverObject | Assert-SqlLogin -Principal 'MyLogin'

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

.EXAMPLE
$serverObject = Connect-SqlDscDatabaseEngine -InstanceName 'MyInstance'
Assert-SqlLogin -ServerObject $serverObject -Principal '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-SqlLogin
{
[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]
$Principal
)

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

if (-not $ServerObject.Logins[$Principal])
{
$missingLoginMessage = $script:localizedData.AssertLogin_LoginMissing -f $Principal, $ServerObject.InstanceName

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

Write-Debug -Message ($script:localizedData.AssertLogin_LoginExists -f $Principal)
}
}
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-SqlLogin
AssertLogin_CheckingLogin = Checking if the principal '{0}' exists as a login on the instance '{1}'.
AssertLogin_LoginMissing = The principal '{0}' does not exist as a login on the instance '{1}'.
AssertLogin_LoginExists = The principal '{0}' exists as a login.
'@
78 changes: 78 additions & 0 deletions tests/Integration/Commands/Assert-SqlLogin.Integration.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
[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-SqlLogin' -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:serverObject = Connect-SqlDscDatabaseEngine -InstanceName $script:instanceName
}

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

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

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

It 'Should not throw an error for SqlAdmin login' {
{ Assert-SqlLogin -ServerObject $script:serverObject -Principal ('{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-SqlLogin -ServerObject $script:serverObject -Principal 'NonExistentLogin123' } | Should -Throw -ExpectedMessage "*does not exist as a login*"
}

It 'Should throw an error with ObjectNotFound category' {
try
{
Assert-SqlLogin -ServerObject $script:serverObject -Principal 'NonExistentLogin123'
}
catch
{
$_.CategoryInfo.Category | Should -Be 'ObjectNotFound'
}
}
}
}
}
131 changes: 131 additions & 0 deletions tests/Unit/Public/Assert-SqlLogin.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
[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-SqlLogin' -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
$mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Logins' -Value {
return @{
'TestLogin' = New-Object -TypeName Object |
Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'TestLogin' -PassThru -Force
}
} -Force
}

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

It 'Should accept ServerObject from pipeline' {
{ $mockServerObject | Assert-SqlLogin -Principal '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
$mockServerObject | Add-Member -MemberType 'ScriptProperty' -Name 'Logins' -Value {
return @{
'ExistingLogin' = New-Object -TypeName Object |
Add-Member -MemberType 'NoteProperty' -Name 'Name' -Value 'ExistingLogin' -PassThru -Force
}
} -Force
}

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

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

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

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

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-SqlLogin').Parameters['ServerObject']
$parameterInfo.Attributes.Mandatory | Should -Contain $true
}

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

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