diff --git a/DSCResources/MSFT_xADCommon/MSFT_xADCommon.ps1 b/DSCResources/MSFT_xADCommon/MSFT_xADCommon.ps1 index 8124e66a4..52be79d99 100644 --- a/DSCResources/MSFT_xADCommon/MSFT_xADCommon.ps1 +++ b/DSCResources/MSFT_xADCommon/MSFT_xADCommon.ps1 @@ -9,12 +9,13 @@ data localizedString IncludeAndExcludeConflictError = The member '{0}' is included in both '{1}' and '{2}' parameter values. The same member must not be included in both '{1}' and '{2}' parameter values. IncludeAndExcludeAreEmptyError = The '{0}' and '{1}' parameters are either both null or empty. At least one member must be specified in one of these parameters. - CheckingMembers = Checking for '{0}' members. + CheckingMembers = Checking for '{0}' members. MembershipCountMismatch = Membership count is not correct. Expected '{0}' members, actual '{1}' members. MemberNotInDesiredState = Member '{0}' is not in the desired state. RemovingDuplicateMember = Removing duplicate member '{0}' definition. MembershipInDesiredState = Membership is in the desired state. - MembershipNotDesiredState = Membership is NOT in the desired state. + MembershipNotDesiredState = Membership is NOT in the desired state. + CheckingDomain = Checking for domain '{0}'. '@ } @@ -38,15 +39,18 @@ function Assert-Module # Internal function to test whether computer is a member of a domain function Test-DomainMember { [CmdletBinding()] + [OutputType([System.Boolean])] param ( ) - $isDomainMember = [System.Boolean] (Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).PartOfDomain; + $isDomainMember = [System.Boolean] (Get-CimInstance -ClassName Win32_ComputerSystem -Verbose:$false).PartOfDomain; return $isDomainMember; } +# Internal function to build domain FQDN function Resolve-DomainFQDN { [CmdletBinding()] param ( [Parameter(Mandatory)] + [OutputType([System.String])] [System.String] $DomainName, [Parameter()] [AllowNull()] @@ -60,6 +64,32 @@ function Resolve-DomainFQDN { return $domainFQDN } +## Internal function to test/ domain availability +function Test-ADDomain +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory)] + [String] $DomainName, + + [Parameter()] + [PSCredential] $Credential + ) + Write-Verbose -Message ($localizedString.CheckingDomain -f $DomainName); + $ldapDomain = 'LDAP://{0}' -f $DomainName; + if ($PSBoundParameters.ContainsKey('Credential')) + { + $domain = New-Object DirectoryServices.DirectoryEntry($ldapDomain, $Credential.UserName, $Credential.GetNetworkCredential().Password); + } + else + { + $domain = New-Object DirectoryServices.DirectoryEntry($ldapDomain); + } + return ($null -ne $domain); +} + # Internal function to get an Active Directory object's parent Distinguished Name function Get-ADObjectParentDN { @@ -88,6 +118,7 @@ function Get-ADObjectParentDN http://www.uvm.edu/~gcd/code-license/ #> [CmdletBinding()] + [OutputType([System.String])] param ( [Parameter(Mandatory)] diff --git a/DSCResources/MSFT_xWaitForADDomain/MSFT_xWaitForADDomain.psm1 b/DSCResources/MSFT_xWaitForADDomain/MSFT_xWaitForADDomain.psm1 index 23c8c9ed7..1f429a57e 100644 --- a/DSCResources/MSFT_xWaitForADDomain/MSFT_xWaitForADDomain.psm1 +++ b/DSCResources/MSFT_xWaitForADDomain/MSFT_xWaitForADDomain.psm1 @@ -13,11 +13,16 @@ function Get-TargetResource [UInt32]$RetryCount = 5 ) - - $convertToCimCredential = New-CimInstance -ClassName MSFT_Credential -Property @{Username=[string]$DomainUserCredential.UserName; Password=[string]$null} -Namespace root/microsoft/windows/desiredstateconfiguration -ClientOnly - + $cimInstanceParams = @{ + ClassName = 'MSFT_Credential' + Property = @{Username=[string]$DomainUserCredential.UserName; Password=[string]$null} + Namespace = 'root/microsoft/windows/desiredstateconfiguration' + ClientOnly = $true + } + $convertToCimCredential = New-CimInstance @cimInstanceParams + $domain = Get-Domain @PSBoundParameters $returnValue = @{ - DomainName = $DomainName + DomainName = $domain.name DomainUserCredential = $convertToCimCredential RetryIntervalSec = $RetryIntervalSec RetryCount = $RetryCount @@ -42,18 +47,17 @@ function Set-TargetResource ) $domainFound = $false - Write-Verbose -Message "Checking for domain $DomainName ..." - + for($count = 0; $count -lt $RetryCount; $count++) { - try + $domain = Get-Domain @PSBoundParameters + if ($domain.name) { - $domain = Get-ADDomain -Identity $DomainName -Credential $DomainUserCredential Write-Verbose -Message "Found domain $DomainName" $domainFound = $true - break; + break } - Catch + else { Write-Verbose -Message "Domain $DomainName not found. Will retry again after $RetryIntervalSec sec" Start-Sleep -Seconds $RetryIntervalSec @@ -80,17 +84,38 @@ function Test-TargetResource [UInt32]$RetryCount = 5 ) - Write-Verbose -Message "Checking for domain $DomainName ..." - try + $domain = Get-Domain @PSBoundParameters + if ($domain.name) { - $domain = Get-ADDomain -Identity $DomainName -Credential $DomainUserCredential Write-Verbose -Message "Found domain $DomainName" $true } - Catch + else { Write-Verbose -Message "Domain $DomainName not found" $false } } +function Get-Domain +{ + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory)] + [String]$DomainName, + + [Parameter(Mandatory)] + [PSCredential]$DomainUserCredential, + + [UInt64]$RetryIntervalSec = 10, + + [UInt32]$RetryCount = 5 + ) + Write-Verbose -Message "Checking for domain $DomainName ..." + New-Object DirectoryServices.DirectoryEntry( + "LDAP://$DomainName", + $DomainUserCredential.UserName, + $DomainUserCredential.GetNetworkCredential().Password + ) +} diff --git a/README.md b/README.md index 41c68d7d6..756915ee4 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ The xADOrganizational Unit DSC resource will manage OUs within Active Directory. ### Unreleased +* xWaitForADDomain: Updated to make it compatible with systems that don't have the ActiveDirectory module installed, and to allow it to function with domains/forests that don't have a domain controller with Active Directory Web Services running. * xADDomain: Added check for Active Directory cmdlets. * xADDomain: Added additional error trapping, verbose and diagnostic information. * xADDomain: Added unit test coverage. diff --git a/Tests/Unit/MSFT_xWaitForADDomain.Tests.ps1 b/Tests/Unit/MSFT_xWaitForADDomain.Tests.ps1 new file mode 100644 index 000000000..410cf4fbb --- /dev/null +++ b/Tests/Unit/MSFT_xWaitForADDomain.Tests.ps1 @@ -0,0 +1,151 @@ +$Global:DSCModuleName = 'xActiveDirectory' +$Global:DSCResourceName = 'MSFT_xWaitForADDomain' + +#region HEADER +[String] $moduleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)) +if ( (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone','https://github.com/PowerShell/DscResource.Tests.git',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\')) +} +else +{ + & git @('-C',(Join-Path -Path $moduleRoot -ChildPath '\DSCResource.Tests\'),'pull') +} +Import-Module (Join-Path -Path $moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $Global:DSCModuleName ` + -DSCResourceName $Global:DSCResourceName ` + -TestType Unit +#endregion + +# Begin Testing +try +{ + + #region Pester Tests + + # The InModuleScope command allows you to perform white-box unit testing on the internal + # (non-exported) code of a Script Module. + InModuleScope $Global:DSCResourceName { + + #region Pester Test Initialization + $password = 'Password' | ConvertTo-SecureString -AsPlainText -Force + $DomainUserCredential = New-Object pscredential('Username', $password) + $domainName = 'example.com' + $testParams = @{ + DomainName = $domainName + DomainUserCredential = $DomainUserCredential + RetryIntervalSec = 10 + RetryCount = 5 + } + $fakeDomainObject = @{Name = $domainName} + #endregion + + + #region Function Get-TargetResource + Describe "$($Global:DSCResourceName)\Get-TargetResource" { + It 'Returns a "System.Collections.Hashtable" object type' { + Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} + $targetResource = Get-TargetResource @testParams + $targetResource -is [System.Collections.Hashtable] | Should Be $true + } + + It "Returns DomainName = $($testParams.DomainName) when domain is found" { + Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} + $targetResource = Get-TargetResource @testParams + $targetResource.DomainName | Should Be $testParams.DomainName + } + + It "Returns an empty DomainName when domain is not found" { + Mock -CommandName Get-Domain -MockWith {} + $targetResource = Get-TargetResource @testParams + $targetResource.DomainName | Should Be $null + } + } + #endregion + + + #region Function Test-TargetResource + Describe "$($Global:DSCResourceName)\Test-TargetResource" { + It 'Returns a "System.Boolean" object type' { + Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} + $targetResource = Test-TargetResource @testParams + $targetResource -is [System.Boolean] | Should Be $true + } + + It 'Passes when domain found' { + Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} + Test-TargetResource @testParams | Should Be $true + } + + It 'Fails when domain not found' { + Mock -CommandName Get-Domain -MockWith {} + Test-TargetResource @testParams | Should Be $false + } + } + #endregion + + + #region Function Set-TargetResource + Describe "$($Global:DSCResourceName)\Set-TargetResource" { + It "Doesn't throw exception and doesn't call Start-Sleep and Clear-DnsClientCache when domain found" { + Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} + Mock -CommandName Start-Sleep -MockWith {} + Mock -CommandName Clear-DnsClientCache -MockWith {} + {Set-TargetResource @testParams} | Should Not Throw + Assert-MockCalled -CommandName Get-Domain -Times 1 -Exactly -Scope It + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Scope It + Assert-MockCalled -CommandName Clear-DnsClientCache -Times 0 -Scope It + } + + It "Doesn't call Start-Sleep and Clear-DnsClientCache when domain found" { + Mock -CommandName Get-Domain -MockWith {return $fakeDomainObject} + Mock -CommandName Start-Sleep -MockWith {} + Mock -CommandName Clear-DnsClientCache -MockWith {} + {Set-TargetResource @testParams} | Should Not Throw + Assert-MockCalled -CommandName Start-Sleep -Times 0 -Scope It + Assert-MockCalled -CommandName Clear-DnsClientCache -Times 0 -Scope It + } + + It "Throws exception when domain not found after $($testParams.RetryCount) retries" { + Mock -CommandName Get-Domain -MockWith {} + Mock -CommandName Start-Sleep -MockWith {} + Mock -CommandName Clear-DnsClientCache -MockWith {} + {Set-TargetResource @testParams} | Should Throw + } + + It "Calls Get-Domain exactly $($testParams.RetryCount) times when domain not found" { + Mock -CommandName Get-Domain -MockWith {} + Mock -CommandName Start-Sleep -MockWith {} + Mock -CommandName Clear-DnsClientCache -MockWith {} + {Set-TargetResource @testParams} | Should Throw + Assert-MockCalled -CommandName Get-Domain -Times $testParams.RetryCount -Exactly -Scope It + } + + It "Calls Start-Sleep exactly $($testParams.RetryCount) times when domain not found" { + Mock -CommandName Get-Domain -MockWith {} + Mock -CommandName Start-Sleep -MockWith {} + Mock -CommandName Clear-DnsClientCache -MockWith {} + {Set-TargetResource @testParams} | Should Throw + Assert-MockCalled -CommandName Start-Sleep -Times $testParams.RetryCount -Exactly -Scope It + } + + It "Calls Clear-DnsClientCache exactly $($testParams.RetryCount) times when domain not found" { + Mock -CommandName Get-Domain -MockWith {} + Mock -CommandName Start-Sleep -MockWith {} + Mock -CommandName Clear-DnsClientCache -MockWith {} + {Set-TargetResource @testParams} | Should Throw + Assert-MockCalled -CommandName Clear-DnsClientCache -Times $testParams.RetryCount -Exactly -Scope It + } + } + #endregion + } + #endregion +} +finally +{ + #region FOOTER + Restore-TestEnvironment -TestEnvironment $TestEnvironment + #endregion +}