Skip to main content

PowerShell + AD + Pester : Create new user using template - Day 1

I did a blog post, way back to create new users in AD using already existing user as a template, but many people commented about using the template didn't copy the Home Directory, logon script path, group membership etc. So finally I tried my hands on writing a Function which does a better job at this.

The idea is to write a New-ADUserFromTemplate function, to which you specify all the properties you want copied while creating a User from an existing User (template User).


Let's make it fun and write the code using the Behavior Driven development approach using Pester. This will probably a 2 part series :

  • Day 1 - Getting the Ground Work ready, Pester tests for Parameter, Help & Module dependency.
  • Day 2 - Write Pester tests and code for the actual function. Refactoring the Code.

So we plan to do BDD or TDD here which means we write tests first and then follow the below cycle :






Disclaimer - Not an expert on BDD/TDD , but constructive feedback is always welcomed.



Let's start by creating a fixture named New-ADUserTemplate using the New-Fixture Function:
PS>New-Fixture -Path .\New-ADUserFromTemplate -Name New-ADUserFromTemplate 
This creates a folder named NewADUserFromTemplate with two files inside it. 
PS>ls

    Directory: C:\Users\Deepak_Dhami\Documents\WindowsPowerShell\Scripts\New-ADUserFromTemplate


Mode                LastWriteTime     Length Name
----                -------------     ------ ----
-a---         8/16/2015   7:43 AM       2054 New-ADUserFromTemplate.ps1
-a---         8/16/2015   6:39 AM       2162 New-ADUserFromTemplate.Tests.ps1


First one is an empty PS1 file with an empty Function definition for New-ADUserFromTemplate and other one matching *.Tests.ps1 is the File where our tests will live. If you haven't picked up on Pester yet then check out the resources section.

So we start by writing tests which define behavior of our code, at first we have any  empty function. So obviously our tests will fail (have patience). Now the next thing on our mind should not be to write an advanced function in PowerShell, instead write simplest code which passes all test. The idea is to get it right first before putting in all fancy stuff there.

Once the test start passing which means my code has achieved the behavior, I wanted.Now it is time to refactor the code (keep running tests when you modify code to make sure the code's behavior hasn't changed).



You won't realize the tremendous benefit this will have to your Scripting effforts until your Function or Module starts growing exponentially and when it does you would be thankful to yourself that you modeled the code properly from start. Below pic sums what I meant to say here :

Credits - memegenerator.net
So let's do it.

First determine the behavior of the Function which we are going to write. The thing which comes to my mind at first is to make sure that I put help and correct parameters in my function.

For the code I am writing I want to mimic my parameters to the ones which the AD User and Computers Snap-in GUI provides while copying a User, using a template. (select User > Right click "Copy").




Let's define the behavior of the Function:
  • It should have inbuilt help along with Description and examples.
  • It should have SamAccountName and GivenName (FirstName is givennameas Mandatory parameters. Also it should have mandatory Instance parameter which takes a AD User Object as input to use as a template.
  • My Function will depend on ActiveDirectory PS Module.
  • It should take the OU path from the Template User.
  • Based on some Constraints in AD Schema, we can only copy few attributes from a template User. Make sure allowed attributes if present on the template User are copied to the new User.
  • It should allow us to select a subset of allowed attributes to copy.
Wow that is easy, now let's rewrite our function's behavior in Pester. I will write the Pester tests and side by side refactor my code so that tests pass. 

To begin with Pester tests have this File preamble , where we dot source our Function/Module which we will test along with any helper functions. Below is how a Pester tests file looks :



Note that I named my Describe block "New-ADUserFromTemplate", as that is exactly what I am doing describing behavior of my Function to Pester.

In file preamble, I load the New-ADUserTemplate.ps1 along with HelperFunctions.ps1 (contains 2 functions named : Test-MandatoryParam & Compare-ADUser).
 

I tend to organize my tests in the Context of the testing I am doing. So you would see in above screenshot that there are 3 Context blocks which are organized in my mind in below way:
  1. Context "Help and Parameter Checks"
      
    It should have inbuilt help along with Description and examples.
      It should have SamAccountName GivenName & Instance as mandatory parameters.
  2. Context "ActiveDirectory Module Checks"
      It Should fail if the AD Module not present.
      It Should fail if the AD PSDrive is not loaded. (Extreme Case)
  3. Context "User Creation"
      It should take OU Path from template User.
      It should only copy allowed set of attributes from the User (by default).
      It should allow copying a subset of allowed set of attributes.  
See piece of cake, If you have thought over, how the code will behave before hand and written tests describing it then half of the job is done.

Let's start with the first Context block 
"Help and Parameter Checks".
Inside the Context block, we have It block which is essentially a unit test. It is inside this IT block that we put our assertions (in layman terms these are comparison between the expected and actual output etc.).
Below is  how my first Context block looks like :
   
001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
    Context "Help and Parameter checks" {
        Set-StrictMode -Version latest
       
        It 'should have inbuilt help along with Description and examples' {
            $helpinfo = Get-Help New-ADUserFromTemplate
            $helpinfo.examples | should not BeNullOrEmpty  # should have examples
            $helpinfo.Details | Should not BeNullOrEmpty   # Should have Details in the Help
            $helpinfo.Description | Should not BeNullOrEmpty # Should have a Descriptiong for the Function
        }
    
        It 'Should have SamAccountName, GivenName & Instance Mandatory params' {
            # {New-ADuserFromTemplate} | Should Throw
            {New-ADuserFromTemplate -samAccountName $null } | should throw
            {New-ADuserFromTemplate -GivenName $null} | should throw
            {New-ADuserFromTemplate -Instance $null } | should throw
            {New-ADuserFromTemplate -GivenName $Null -SamAccountName $null -Instance $Null } | Should Throw
        }
    } # end Context

First IT block tests that my Function help always has Examples, Details and Description. Credits to Andy Schnieder for sharing this.

Second IT block is a bit tricky here. I defined that the parameters SamAccountName and GivenName be mandatory parameters for my Function. Now the first assertion which naturally comes to mind is :
{New-ADuserFromTemplate| should throw 

But this won't work, we will see later why. So as a temporary workaround of not specifying anything to Mandatory parameters, I am passing them $Null.
{New-ADuserFromTemplate -samAccountName $null } | should throw

There are some otherways to look at the Function metadata (MVP Dave Wyatt pointed out that to me) but that will make this post deviate from the original objective. So my tests which define the help & parameters behavior on my function are ready.

Let's go through the Red phase first :

PS>invoke-pester -TestName 'New-ADUserFromTemplate' 
Executing all tests in 'C:\Users\DexterPOSH\Documents\WindowsPowerShell\Pester_Tests\new-aduserfromtemplate' matching test name 'New-ADUserFromTemplate'
Describing New-ADUserFromTemplate
   Context Help and Parameter checks
    [-] should have inbuilt help along with Description and examples 204ms
      Expected: value to not be empty
      at line: 16 in C:\Users\Deepak_Dhami\Documents\WindowsPowerShell\Pester_Tests\new-aduserfromtemplate\New-ADUserFromTemplate.Tests.ps1
    [-] Should have SamAccountName & GivenName as Mandatory params 37ms
      Expected: the expression to throw an exception
      at line: 22 in C:\Users\Deepak_Dhami\Documents\WindowsPowerShell\Pester_Tests\new-aduserfromtemplate\New-ADUserFromTemplate.Tests.ps1

All right, now let's go and work on making these tests pass by modifying our New-ADUserFromTemplate function , below is what I added to the Function definition :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
function New-ADUserFromTemplate
{
<#
.Synopsis
   Function which enables creating new users using a Template
.DESCRIPTION
   Function which will use a User as a template and then copy set of below attributes to the new user.

.EXAMPLE
    First get the AD user Stored in a variable with all the properties (it copies only a subset of properties on the Object supplied)
    PS> $TemplateUser = Get-ADUser -identity Test1 -Properties *
    PS> New-ADUserFromTemplate -SamAccountname newuser123 -GivenName NewUser -Instance $TemplateUser
.EXAMPLE
   If the AD User Object doesn't have all the Properties on it then the Function only selects the available ones.
    PS> $TemplateUser = Get-ADUser -identity Test1
    PS> New-ADUserFromTemplate -SamAccountname newuser123 -GivenName NewUser -Instance $TemplateUser
#>

[CmdletBinding()]
   param(
        [Parameter(Mandatory=$True)]    
        [string]$SamAccountName,

        [Parameter(Mandatory)]    
        [string]$GivenName,

        [Parameter(Mandatory)]
        [Microsoft.ActiveDirectory.Management.ADUser]$Instance
   )
}

Once this is done, I invoke the Pester again and see the test in the Context passing, But :




Let's remove the blocking test and run the Pester tests again, we see Green :



Now it is time to refactor. Make sure that after any changes you make, run Pester to validate that nothing has changed.

Moving on to the next Context of testing. It looks like below :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
    Context "ActiveDirectory Module Available" {     
        $TemplateUser = [pscustomobject]@{
                                            Name='testuser'
                                            UserPrincipalName='testuser@dex.com'
                                            PStypeName = 'Microsoft.ActiveDirectory.Management.ADUser'
                                            }
     

        It "Should Fail if the AD Module not present" {
            Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable
            {New-ADUserFromTemplate -SamAccountName test123 -GivenName 'test 123' -Instance $TemplateUser } | should throw         
            Assert-VerifiableMocks
        }     
    }

Note that in the Context scope, a Custom Object $TemplateUser in initialized, which will be passed later on to the Function (mandatory).

Now , the problem is my machine has the AD Module, so how does I simulate a situation where the machine doesn't have the AD module. This is where mocking comes into picture.


In a machine where the AD module is present, if I run below: 

Import-Module -name ActiveDirectory -ErrorAction stop

It would succeed, but in a machine which doesn't have the module named ActiveDirectory an exception will be thrown. This would be our mock.
Mock -CommandName Import-Module -ParameterFilter {$name -eq 'ActiveDirectory'} `
     -MockWith {Throw (New-Object -TypeName System.IO.FileNotFoundException)} -Verifiable 

Above, We mock -> Import-Module -name ActiveDirectory . Notice -ParameterFilter {$name -eq 'ActiveDirectory' mocks only the Import-Module cmdlet when it is passed an argument 'ActiveDirectory' to the -Name parameter. 

Also the  -Verifiable switch at the end of the mock makes it verifiable, how ?
Simple use 
Assert-VerifiableMocks at the end in the IT block. It will verify that the mocks with -Verifiable switch were called in the Function run.

After writing tests, run them (Red Phase), then write the bare minimum code to make it pass (Green) and keep refactoring. Skipping Red and jumping to Green for the above context (as you already have an idea).

My bare minimum Function definition passing both the Context tests is below :


001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
function New-ADUserFromTemplate
{
<#
.Synopsis
   Function which enables creating new users using a Template
.DESCRIPTION
   Function which will use a User as a template and then copy set of below attributes to the new user.

.EXAMPLE
    First get the AD user Stored in a variable with all the properties (it copies only a subset of properties on the Object supplied)
    PS> $TemplateUser = Get-ADUser -identity Test1 -Properties *
    PS> New-ADUserFromTemplate -SamAccountname newuser123 -GivenName NewUser -Instance $TemplateUser
.EXAMPLE
   If the AD User Object doesn't have all the Properties on it then the Function only selects the available ones.
    PS> $TemplateUser = Get-ADUser -identity Test1
    PS> New-ADUserFromTemplate -SamAccountname newuser123 -GivenName NewUser -Instance $TemplateUser
#>

[CmdletBinding()]
   param(
        [Parameter(Mandatory=$True)]     
        [string]$SamAccountName,

        [Parameter(Mandatory)]     
        [string]$GivenName,

        [Parameter(Mandatory)]
        [Object]$Instance
   )
    TRY {
        # try to import the Module
        Import-Module -name ActiveDirectory -ErrorAction stop
        $null = Get-PSDrive -Name AD -ErrorAction stop  # Query if the AD PSdrive is loaded
    
    }
    CATCH [System.IO.FileNotFoundException]{
        Write-Warning -Message $_.exception
        throw "AD module not found"
    }
    CATCH {
        throw $_.exception
    }
}

Below is the result of my pester tests in Green phase :


PS>invoke-pester

Describing New-ADUserFromTemplate
   Context Help and Parameter checks
    [+] should have inbuilt help along with Description and examples 233ms
    [+] Should have SamAccountName, GivenName  & Instance Mandatory params 57ms
   Context ActiveDirectory Module Available
WARNING: Unable to find the specified file.
    [+] Should Fail if the AD Module not present 106ms


Oh My God !  Why to go to this length trouble for a small function ?

You are right ! It is a whole lot of trouble but when this function starts to grow or it becomes part of a bigger module or Script running in Production. Running tests and seeing them pass would give you "Confidence" or "Trust" in your code.

Also you don't have to test each scenario manually ;)

In next post, we will dive into writing tests (first) and code which copies the attributes for a User from a template user. I am presenting on the very same topic at PowerShell Conference Asia @ Singapore next week and I hope to make a strong case for Pester there.

Resources:


Practical PowerShell Unit Testing : Getting Started (Fantastic article)
https://www.simple-talk.com/sysadmin/powershell/practical-powershell-unit-testing-getting-started/


PowerShellMag articles on Pester:
http://www.powershellmagazine.com/tag/pester/

Copy User's Properties

https://technet.microsoft.com/en-us/library/dd378959(v=ws.10).aspx

AD Constraints

https://msdn.microsoft.com/en-us/library/cc223462.aspx




Popular posts from this blog

Test connectivity via a specific network interface

Recently while working on a Private cloud implementation, I came across a scenario where I needed to test connectivity of a node to the AD/DNS via multiple network adapters.  Many of us would know that having multiple network routes is usually done to take care of redundancy. So that if a network adapter goes down, one can use the other network interface to reach out to the node. In order to make it easy for everyone to follow along, below is an analogy for the above scenario: My laptop has multiple network adapters (say Wi-Fi and Ethernet) connected to the same network. Now how do I test connectivity to a Server on the network only over say Wi-Fi network adapter?

PowerShell + SCCM : Run CM cmdlets remotely

Today I saw a tweet about using implicit remoting to load the Configuration Manager on my machine by Justin Mathews . It caught my eye as I have never really tried it, but theoretically it can be done. Note - The second tweet says "Cannot find a provider with the name CMSite", resolution to which is in the Troubleshooting section at the end.

PowerShell : Trust network share to load modules & ps1

Problem Do you have a central network share, where you store all the scripts or PowerShell modules ? What happens if you try to run the script from a network share ? or if you have scripts (local) which invoke scripts or import PowerShell modules stored on this network share ? Well you would see a security warning like below (Note - I have set execution policy as 'Unrestricted' not 'bypass' here): Run a .ps1 from the network share Well this is a similar warning, which you get when you download scripts from Internet. As the message says run Unblock-File cmdlet to unblock the script and then run it, let's try it.