PSADSync.Tests.ps1

,#region import modules
$ThisModule = "$($MyInvocation.MyCommand.Path -replace '\.Tests\.ps1$', '').psd1"
$ThisModuleName = (($ThisModule | Split-Path -Leaf) -replace '\.psd1')
Get-Module -Name $ThisModuleName -All | Remove-Module -Force

Import-Module -Name $ThisModule -Force -ErrorAction Stop
#endregion

describe 'Module-level tests' {
    
    it 'should validate the module manifest' {
    
        { Test-ModuleManifest -Path $ThisModule -ErrorAction Stop } | should not throw
    }

    it 'should pass all error-level script analyzer rules' {

        $excludedRules = @(
            'PSUseShouldProcessForStateChangingFunctions',
            'PSUseToExportFieldsInManifest',
            'PSAvoidInvokingEmptyMembers',
            'PSUsePSCredentialType',
            'PSAvoidUsingPlainTextForPassword'
        )

        Invoke-ScriptAnalyzer -Path $PSScriptRoot -ExcludeRule $excludedRules -Severity Error | should benullorempty
    }
}

InModuleScope $ThisModuleName {

    $script:AllAdsiUsers = 0..10 | ForEach-Object {
        $i = $_
        $adsiUser = New-MockObject -Type 'System.DirectoryServices.AccountManagement.UserPrincipal'
        $amParams = @{
            MemberType = 'NoteProperty'
            Force = $true
        }
        $props = @{
            'Name' = 'nameval'
            'Enabled' = $true
            'SamAccountName' = 'samval'
            'GivenName' = 'givennameval'
            'Surname' = 'surnameval'
            'DisplayName' = 'displaynameval'
            'OtherProperty' = 'otherval'
            'EmployeeId' = 1
            'Title' = 'titleval'
        }
        $props.GetEnumerator() | ForEach-Object {
            if ($_.Key -eq 'Enabled') {
                if ($i % 2) {
                    $adsiUser | Add-Member @amParams -Name $_.Key -Value $false
                } else {
                    $adsiUser | Add-Member @amParams -Name $_.Key -Value $true
                }
            } else {
                $adsiUser | Add-Member @amParams -Name $_.Key -Value "$($_.Value)$i"
            }
        }
        if ($i -eq 5) {
            $adsiUser | Add-Member @amParams -Name 'samAccountName' -Value $null
        }
        if ($i -eq 6) { 
            $adsiUser | Add-Member @amParams -Name 'EmployeeId' -Value $null
        }
        $adsiUser
    }

    $script:AllCsvUsers = 0..15 | ForEach-Object {
        $i = $_
        $output = @{ 
            AD_LOGON = "nameval$i"
            PERSON_NUM = "1$i" 
            ExcludeCol = 'dontexcludeme'
        }
        if ($i -eq (Get-Random -Maximum 9)) {
            $output.'AD_LOGON' = $null
            $output.ExcludeCol = 'excludeme'
        }
        if ($i -eq (Get-Random -Maximum 9)) {
            $output.'PERSON_NUM' = $null
        }
        [pscustomobject]$output 
    }

    describe 'Get-CompanyCsvUser' {
    
        $commandName = 'Get-CompanyCsvUser'
    
        #region Mocks
            $script:csvUsers = @(
                [pscustomobject]@{
                    AD_LOGON = 'foo'
                    PERSON_NUM = 123
                    OtherAtrrib = 'x'
                    ExcludeCol = 'excludeme'
                    ExcludeCol2 = 'dontexcludeme'
                }
                [pscustomobject]@{
                    AD_LOGON = 'foo2'
                    PERSON_NUM = 1234
                    OtherAtrrib = 'x'
                    ExcludeCol = 'dontexcludeme'
                    ExcludeCol2 = 'excludeme'
                }
                [pscustomobject]@{
                    AD_LOGON = 'notinAD'
                    PERSON_NUM = 1234
                    OtherAtrrib = 'x'
                    ExcludeCol = 'dontexcludeme'
                    ExcludeCol2 = 'dontexcludeme'
                }
                [pscustomobject]@{
                    AD_LOGON = $null
                    PERSON_NUM = 12345
                    OtherAtrrib = 'x'
                    ExcludeCol = 'dontexcludeme'
                    ExcludeCol2 = 'dontexcludeme'
                }
            )

            mock 'Import-Csv' {
                $script:csvUsers
            }

            mock 'Test-Path' {
                $true
            }

            $script:csvUsersNullConvert = $script:csvUsers | ForEach-Object { if (-not $_.'AD_LOGON') { $_.'AD_LOGON' = 'null' } $_ }
        #endregion
        
        $parameterSets = @(
            @{
                CsvFilePath = 'C:\users.csv'
                TestName = 'All users'
            }
            @{
                CsvFilePath = 'C:\users.csv'
                Exclude = @{ ExcludeCol = 'excludeme' }
                TestName = 'Exclude 1 col'
            }
            @{
                CsvFilePath = 'C:\users.csv'
                Exclude = @{ ExcludeCol = 'excludeme';ExcludeCol2 = 'excludeme' }
                TestName = 'Exclude 2 cols'
            }
        )
    
        $testCases = @{
            All = $parameterSets
            Exclude = $parameterSets.where({$_.ContainsKey('Exclude')})
            Exclude1Col = $parameterSets.where({$_.ContainsKey('Exclude') -and ($_.Exclude.Keys.Count -eq 1)})
            Exclude2Cols = $parameterSets.where({$_.ContainsKey('Exclude') -and ($_.Exclude.Keys.Count -eq 2)})
            NoExclusions = $parameterSets.where({ -not $_.ContainsKey('Exclude')})
        }

        context 'when at least one column is excluded' {

            mock 'Where-Object' {
                [pscustomobject]@{
                    AD_LOGON = 'foo2'
                    PERSON_NUM = 1234
                    OtherAtrrib = 'x'
                    ExcludeCol = 'dontexcludeme'
                    ExcludeCol2 = 'excludeme'
                }
                [pscustomobject]@{
                    AD_LOGON = 'notinAD'
                    PERSON_NUM = 1234
                    OtherAtrrib = 'x'
                    ExcludeCol = 'dontexcludeme'
                    ExcludeCol2 = 'dontexcludeme'
                }
                [pscustomobject]@{
                    AD_LOGON = $null
                    PERSON_NUM = 12345
                    OtherAtrrib = 'x'
                    ExcludeCol = 'dontexcludeme'
                    ExcludeCol2 = 'dontexcludeme'
                }
            } -ParameterFilter { $FilterScript.ToString() -notmatch '\*' }
        
            it 'should create the expected where filter: <TestName>' -TestCases $testCases.Exclude {
                param($CsvFilePath,$Exclude)
            
                & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'Where-Object'
                    Times = $script:csvUsers.Count
                    Exactly = $true
                    Scope = 'It'
                    ParameterFilter = { 
                        $PSBoundParameters.FilterScript.ToString() -like "(`$_.`'*' -ne '*')*" }
                }
                Assert-MockCalled @assMParams
            }
        
        }

        it 'when excluding no cols, should return all expected users: <TestName>' -TestCases $testCases.NoExclusions {
            param($CsvFilePath,$Exclude)
        
            $result = & $commandName @PSBoundParameters

            (Compare-Object $script:csvUsersNullConvert.'AD_LOGON' $result.'AD_LOGON').InputObject | should benullorempty
        }

        it 'when excluding 1 col, should return all expected users: <TestName>' -TestCases $testCases.Exclude1Col {
            param($CsvFilePath,$Exclude)
        
            $result = & $commandName @PSBoundParameters

            (Compare-Object @('foo2','notinAD','null') $result.'AD_LOGON').InputObject | should benullorempty
        }
    
        it 'when excluding 2 cols, should return all expected users: <TestName>' -TestCases $testCases.Exclude2Cols {
            param($CsvFilePath,$Exclude)
        
            $result = & $commandName @PSBoundParameters

            (Compare-Object @('notinAD','null') $result.'AD_LOGON').InputObject | should benullorempty
        }
    }

    describe 'GetCsvColumnHeaders' {
        
        #region Mocks
            mock 'Get-Content' {
                @(
                    '"Header1","Header2","Header3"'
                    '"Value1","Value2","Value3"'
                    '"Value4","Value5","Value6"'
                )
            }
        #endregion

        it 'should return expected headers' {
        
            $result = & GetCsvColumnHeaders -CsvFilePath 'foo.csv'
            Compare-Object $result @('Header1','Header2','Header3') | should benullorempty
        }
        
    }

    describe 'TestCsvHeaderExists' {
        
        #region Mocks
            mock 'GetCsvColumnHeaders' {
                'Header1','Header2','Header3'
            }
        #endregion


        context 'when a header is not in the CSV' {
        
            it 'should return $false' {
            
                TestCsvHeaderExists -CsvFilePath 'foo.csv' -Header 'nothere' | should be $false
            }    
        
        }

        context 'when all headers are in the CSV' {

            it 'should return $true' {
                TestCsvHeaderExists -CsvFilePath 'foo.csv' -Header 'Header1','Header2','Header3' | should be $true
            }
    
        }

        context 'when one header is in the CSV' {

            it 'should return $true' {        
                TestCsvHeaderExists -CsvFilePath 'foo.csv' -Header 'Header1' | should be $true
            }

        }
        
    }

    describe 'Get-CompanyAdUser' {
    
        $commandName = 'Get-CompanyAdUser'

    
        #region Mocks
            mock 'GetAdUser' {
                $script:AllAdsiUsers | Where-Object { $_.Enabled }
            } -ParameterFilter { $LdapFilter }

            mock 'GetAdUser' {
                $script:AllAdsiUsers
            } -ParameterFilter { -not $LdapFilter }
        #endregion
        
        $parameterSets = @(
            @{
                FieldMatchMap = @{ 'csvid' = 'adid' }
                TestName = 'All users'
            }
        )
    
        $testCases = @{
            All = $parameterSets
        }

        it 'should return all users: <TestName>' -TestCases $testCases.All {
            param($All,$Credential,$FieldMatchMap)
        
            $result = & $commandName @PSBoundParameters
            @($result).Count | should be @($script:AllAdsiUsers).Count
        }

    }

    describe 'FindUserMatch' {
    
        $commandName = 'FindUserMatch'
        
    
        #region Mocks
            mock 'Write-Warning'

            $script:csvUserMatchOnOneIdentifer = @(
                [pscustomobject]@{
                    AD_LOGON = 'foo'
                    PERSON_NUM = 'nomatch'
                }
            )

            $script:csvUserMatchOnAllIdentifers = @(
                [pscustomobject]@{
                    AD_LOGON = 'foo'
                    PERSON_NUM = 123
                }
            )

            $script:OneblankCsvUserIdentifier = @(
                [pscustomobject]@{
                    PERSON_NUM = $null
                    AD_LOGON = 'foo'
                }
            )

            $script:AllblankCsvUserIdentifier = @(
                [pscustomobject]@{
                    AD_LOGON = $null
                    PERSON_NUM = $null
                }
            )
            
            $script:noBlankCsvUserIdentifier = @(
                [pscustomobject]@{
                    AD_LOGON = 'ffff'
                    PERSON_NUM = '111111'
                }
            )

            $script:csvUserNoMatch = @(
                [pscustomobject]@{
                    AD_LOGON = 'NotInAd'
                    PERSON_NUM = 'nomatch'
                }
            )

            $script:AdUsers = @(
                [pscustomobject]@{
                    samAccountName = 'foo'
                    EmployeeId = 123
                }
                [pscustomobject]@{
                    samAccountName = 'foo2'
                    EmployeeId = 111
                }
                [pscustomobject]@{
                    samAccountName = 'NotinCSV'
                    EmployeeId = 12345
                }
            )

            mock 'Write-Verbose'
        #endregion
        
        $parameterSets = @(
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:csvUserMatchOnOneIdentifer
                FieldMatchMap = @{ 'AD_LOGON' = 'samAccountName' }
                TestName = 'Match on 1 ID'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:csvUserMatchOnAllIdentifers
                FieldMatchMap = @{ 'PERSON_NUM' = 'EmployeeId' }
                TestName = 'Match on all IDs'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:csvUserNoMatch
                FieldMatchMap = @{ 'PERSON_NUM' = 'EmployeeId' }
                TestName = 'No Match'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:OneblankCsvUserIdentifier
                FieldMatchMap = [ordered]@{ 
                    'PERSON_NUM' = 'EmployeeId'
                    'AD_LOGON' = 'samAccountName'
                }
                TestName = 'One Blank ID'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:AllblankCsvUserIdentifier
                FieldMatchMap = @{ 
                    'PERSON_NUM' = 'EmployeeId'
                    'AD_LOGON' = 'samAccountName'
                }
                TestName = 'All Blank IDs'
            }
        )
    
        $testCases = @{
            All = $parameterSets
            MatchOnOneId = $parameterSets.where({$_.TestName -eq 'Match on 1 ID'})
            MatchOnAllIds = $parameterSets.where({$_.TestName -eq 'Match on all IDs'})
            NoMatch = $parameterSets.where({$_.TestName -eq 'No Match'})
            OneBlankId = $parameterSets.where({ $_.CsvUser.AD_LOGON -and -not $_.CsvUser.PERSON_NUM })
            AllBlankIds = $parameterSets.where({ -not $_.CsvUser.AD_LOGON -and (-not $_.CsvUser.PERSON_NUM) })
        }

        context 'When no matches could be found' {
            it 'should return the expected number of objects: <TestName>' -TestCases $testCases.NoMatch {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                & $commandName @PSBoundParameters | should benullorempty
            }
        }

        context 'When one match can be found' {

            it 'should return the expected number of objects: <TestName>' -TestCases $testCases.MatchOnOneId {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                $result = & $commandName @PSBoundParameters
                @($result).Count | should be 1
            }

            it 'should find matches as expected and return the expected property values: <TestName>' -TestCases $testCases.MatchOnOneId {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                $result = & $commandName @PSBoundParameters

                $result.MatchedAdUser.EmployeeId | should be 123
                $result.CsvIdMatchedOn | should be 'AD_LOGON'
                $result.AdIdMatchedOn | should be 'samAccountName'

            }
        }

        context 'When multiple matches could be found' {

            it 'should return the expected number of objects: <TestName>' -TestCases $testCases.MatchOnAllIds {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                $result = & $commandName @PSBoundParameters
                @($result).Count | should be 1
            }

            it 'should find matches as expected and return the expected property values: <TestName>' -TestCases $testCases.MatchOnAllIds {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                $result = & $commandName @PSBoundParameters

                $result.MatchedAdUser.EmployeeId | should be 123
                $result.CsvIdMatchedOn | should be 'PERSON_NUM'
                $result.AdIdMatchedOn | should be 'employeeid'

            }
        }

        context 'when a blank identifier is queried before finding a match' {

            it 'should do nothing: <TestName>' -TestCases $testCases.OneBlankId {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'Write-Verbose'
                    Times = 1
                    Exactly = $true
                    Scope = 'It'
                    ParameterFilter = { $PSBoundParameters.Message -match '^CSV field match value' }
                }
                Assert-MockCalled @assMParams
            }

            it 'should return the expected object properties: <TestName>' -TestCases $testCases.OneBlankId {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                $result = & $commandName @PSBoundParameters
                $result.MatchedAdUser.samAccountName | should be 'foo'
                $result.CsvIdMatchedOn | should be 'AD_LOGON'
                $result.AdIdMatchedOn | should be 'samAccountName'
            }

        }

        context 'when all identifers are blank' {

            it 'should do nothing: <TestName>' -TestCases $testCases.AllBlankIds {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'Write-Verbose'
                    Times = 2
                    Exactly = $true
                    Scope = 'It'
                    ParameterFilter = { $PSBoundParameters.Message -match '^CSV field match value' }
                }
                Assert-MockCalled @assMParams
            }

        }

        context 'when all identifiers are valid' {
        
            it 'should return the expected object properties: <TestName>' -TestCases $testCases.MatchOnAllIds {
                param($AdUsers,$CsvUser,$FieldMatchMap)
            
                $result = & $commandName @PSBoundParameters
                @($result.MatchedAdUser).foreach({
                    $_.PSObject.Properties.Name -contains 'EmployeeId' | should be $true
                })
                $result.CsvIdMatchedOn | should be 'PERSON_NUM'
                $result.AdIdMatchedOn | should be 'employeeId'
            }
        
        }
    }

    describe 'FindAttributeMismatch' {
    
        $commandName = 'FindAttributeMismatch'
        
        #region Mocks
            mock 'Write-Verbose'

            $script:csvUserMisMatch = [pscustomobject]@{
                AD_LOGON = 'foo'
                PERSON_NUM = 123
                OtherAttrib = 'x'
            }

            $script:csvUserNoMisMatch = [pscustomobject]@{
                AD_LOGON = 'foo'
                PERSON_NUM = 1111
                OtherAttrib = 'y'
            }

            $script:AdUserMisMatch = New-MockObject -Type 'System.DirectoryServices.AccountManagement.UserPrincipal'
            $script:AdUserMisMatch | Add-Member -MemberType NoteProperty -Name 'samAccountName' -Force -Value 'foo'
            $script:AdUserMisMatch | Add-Member -MemberType NoteProperty -Name 'EmployeeId' -Force -Value $null
            $script:AdUserMisMatch | Add-Member -MemberType NoteProperty -Name 'otherattribmap' -Force -Value $null -PassThru

            $script:AdUserNoMisMatch = New-MockObject -Type 'System.DirectoryServices.AccountManagement.UserPrincipal'
            $script:AdUserNoMisMatch | Add-Member -MemberType NoteProperty -Name 'samAccountName' -Force -Value 'foo'
            $script:AdUserNoMisMatch | Add-Member -MemberType NoteProperty -Name 'EmployeeId' -Force -Value 1111
            $script:AdUserNoMisMatch | Add-Member -MemberType NoteProperty -Name 'otherattribmap' -Force -Value 'y' -PassThru

            mock 'Get-Member' {
                [pscustomobject]@{
                    Name = 'samAccountName'
                }
                [pscustomobject]@{
                    Name = 'EmployeeId'
                }
                [pscustomobject]@{
                    Name = 'otherattribmap'
                }
            }
        #endregion
        
        $parameterSets = @(
            @{
                AdUser = $script:AdUserMisMatch
                CsvUser = $script:csvUserMisMatch
                FieldSyncMap = @{ 'OtherAttrib' = 'otherattribmap' }
                TestName = 'Mismatch'
            }
            @{
                AdUser = $script:AdUserNoMisMatch
                CsvUser = $script:csvUserNoMisMatch
                FieldSyncMap = @{ 'OtherAttrib' = 'otherattribmap' }
                TestName = 'No Mismatch'
            }
        )
    
        $testCases = @{
            All = $parameterSets
            Mismatch = $parameterSets.where({$_.TestName -eq 'Mismatch'})
            NoMismatch = $parameterSets.where({$_.TestName -eq 'No Mismatch'})
        }

        it 'should find the correct AD property names: <TestName>' -TestCases $testCases.All {
            param($AdUser,$CsvUser,$FieldSyncMap)
        
            & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'Write-Verbose'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { 
                    $PSBoundParameters.Message -eq "ADUser props: [samAccountName,EmployeeId,otherattribmap]" }
            }
            Assert-MockCalled @assMParams
        }

        it 'should find the correct CSV property names: <TestName>' -TestCases $testCases.All {
            param($AdUser,$CsvUser,$FieldSyncMap)
        
            & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'Write-Verbose'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { 
                    $PSBoundParameters.Message -eq 'CSV properties are: [AD_LOGON,PERSON_NUM,OtherAttrib]' }
            }

            Assert-MockCalled @assMParams
        }

        context 'when a mismatch is found' {

            it 'should return the expected objects: <TestName>' -TestCases $testCases.Mismatch {
                param($AdUser,$CsvUser,$FieldSyncMap)
            
                $result = & $commandName @PSBoundParameters
                @($result).Count | should be 1
                $result | should beoftype 'hashtable'
                $result.CSVAttributeName | should be 'OtherAttrib'
                $result.CSVAttributeValue | should be 'x'
                $result.ADAttributeName | should be 'otherattribmap'
                $result.ADAttributeValue | should be ''
            }
        }

        context 'when no mismatches are found' {

            it 'should return nothing: <TestName>' -TestCases $testCases.NoMismatch {
                param($AdUser,$CsvUser,$FieldSyncMap)
            
                & $commandName @PSBoundParameters | should benullorempty
            }

        }
        
        context 'when a non-terminating error occurs in the function' {

            mock 'Write-Verbose' {
                Write-Error -Message 'error!'
            }

            it 'should throw an exception: <TestName>' -TestCases $testCases.All {
                param($AdUser,$CsvUser,$FieldSyncMap)
            
                $params = @{} + $PSBoundParameters
                { & $commandName @params } | should throw 'error!'
            }
        }
    
    }

    describe 'SetAduser' {
    
        $commandName = 'SetAduser'
        

        mock 'SaveAdUser'

        mock 'GetAdUser' {
            $obj = New-MockObject -Type 'System.DirectoryServices.SearchResult'
            $obj | Add-Member -MemberType NoteProperty -Name 'Properties' -PassThru -Force -Value ([pscustomobject]@{
                adsPath = 'adspathhere'
            })
        } -ParameterFilter { $OutputAs -eq 'SearchResult' }

        mock 'GetAdUser' {
            New-MockObject -Type 'System.DirectoryServices.AccountManagement.UserPrincipal'
        } -ParameterFilter { $OutputAs -ne 'SearchResult' }
    
        $parameterSets = @(
            @{
                Identity = @{ samAccountName = 'samnamehere'}
                Attribute = @{ employeeId = 'empidhere' }
            }
            @{
                Identity = @{ employeeId = 'empidhere'}
                Attribute = @{ displayName = 'displaynamehere' }
            }
        )
    
        $testCases = @{
            All = $parameterSets
        }
    
        it 'returns nothing' -TestCases $testCases.All {
            param($Identity,$Attribute)

            & $commandName @PSBoundParameters | should benullorempty
        }

        it 'should save the expected attribute' -TestCases $testCases.All {
            param($Identity,$Attribute)
        
            & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'SaveAdUser'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { 
                    (-not (Compare-Object $PSBoundParameters.Parameters.Attribute.Keys $Attribute.Keys)) -and
                    (-not (Compare-Object $PSBoundParameters.Parameters.Attribute.Values $Attribute.Values))
                }
            }
            Assert-MockCalled @assMParams
        }

        it 'should save on the expected identity' -TestCases $testCases.All {
            param($Identity,$Attribute)

            & $commandName @PSBoundParameters
        
            $assMParams = @{
                CommandName = 'SaveAdUser'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { 
                    $PSBoundParameters.Parameters.AdsPath -eq 'adspathhere'
                }
            }
            Assert-MockCalled @assMParams
        }
    
    }

    describe 'SyncCompanyUser' {
    
        $commandName = 'SyncCompanyUser'
        $command = Get-Command -Name $commandName

        $script:AdUserUpn = New-MockObject -Type 'System.DirectoryServices.AccountManagement.UserPrincipal'
        $script:AdUserUpn | Add-Member -MemberType NoteProperty -Name 'samAccountName' -Force -Value 'samaccountnameval'
        $script:AdUserUpn | Add-Member -MemberType NoteProperty -Name 'EmployeeId' -Force -Value 'empidhere' -PassThru

        $script:csvUser = [pscustomobject]@{
            AD_LOGON = 'foo'
            PERSON_NUM = 123
            OtherAtrrib = 'x'
        }
    
        #region Mocks
            mock 'SetAdUser'
        #endregion
    
        $testCases = @(
            @{
                Label = 'SamAccountName Identifier, 1 Attributes hashtable'
                Parameters = @{
                    AdUser = $script:AdUserUpn
                    CsvUser = $script:csvUser
                    Attributes = @{ 
                        ADAttributeName = 'EmployeeId'
                        ADAttributeValue = $null
                        CSVAttributeName = 'username'
                        CSVAttributeValue = 'newattribvalue1'
                    }
                    Identifier = 'samAccountName'
                    Confirm = $false
                }
                Expect = @(
                    @{
                        Type = 'Function parameters'
                        Name = 'SetAdUser'
                        Parameters = @(
                            @{
                                Identity = @{ samAccountName = 'samaccountnameval' }
                                Attribute = @{ EmployeeId = 'newattribvalue1' }
                            }
                        )
                    }
                )
            }
            @{
                Label = 'EmployeeId Identifier, 2 Attributes hashtable'
                Parameters = @{
                    AdUser = $script:AdUserUpn
                    CsvUser = $script:csvUser
                    Attributes = @(
                        @{ 
                            ADAttributeName = 'EmployeeId'
                            ADAttributeValue = $null
                            CSVAttributeName = 'username'
                            CSVAttributeValue = 'newattribvalue1'
                        },
                        @{ 
                            ADAttributeName = 'attribneedchanged'
                            ADAttributeValue = 'thisneedschanged'
                            CSVAttributeName = 'username'
                            CSVAttributeValue = 'newattribvalue2'
                        }
                    )
                    Identifier = 'employeeId'
                    Confirm = $false
                }
                Expect = @(
                    @{
                        Type = 'Function parameters'
                        Name = 'SetAdUser'
                        Parameters = @(
                            @{
                                Identity = @{ employeeId = 'empidhere' }
                                Attribute = @{ EmployeeId = 'newattribvalue1' }
                            }
                            @{
                                Identity = @{ employeeId = 'empidhere' }
                                Attribute = @{ attribneedchanged = 'newattribvalue2' }
                            }
                        )
                    }
                )
            }
        )
    
        foreach ($testCase in $testCases) {
    
            context $testcase.Label {
    
                $expect = $testcase.Expect
                $funcParams = $testCase.Parameters
    
                $result = & $commandName @funcParams
    
                it 'should return nothing' {
                    $result | should benullorempty
                }
    
                it 'should change only those attributes in the Attributes parameter' {

                    $expectedParams = $expect.where({ $_.Name -eq 'SetAdUser'})

                    $assMParams = @{
                        CommandName = 'SetAdUser'
                        Times = @($funcParams.Attributes).Count
                        Exactly = $true
                        ParameterFilter = {
                            foreach ($paramHt in $expectedParams.Parameters) { 
                                $PSBoundParameters.Attribute.Keys -in $paramHt.Attribute.Keys -and
                                $PSBoundParameters.Attribute.Values -in $paramHt.Attribute.Values
                            }
                        }
                    }
                    Assert-MockCalled @assMParams
                }

                it 'should change attributes on the expected user account' {

                    $expectedParams = $expect.where({ $_.Name -eq 'SetAdUser'})

                    $assMParams = @{
                        CommandName = 'SetAdUser'
                        Times = @($funcParams.Attributes).Count
                        Exactly = $true
                        ParameterFilter = {
                            foreach ($i in $expectedParams.Parameters) {
                                $PSBoundParameters.Idenity.Keys -in $i.Idenity.Keys -and
                                $PSBoundParameters.Identity.Values -in $i.Identity.Values
                            }
                        }
                    }
                    Assert-MockCalled @assMParams
                }

                context 'when a non-terminating error occurs in the function' {

                    mock 'Write-Verbose' {
                        Write-Error -Message 'error!'
                    }

                    it 'should throw an exception' {
                    
                        $params = @{} + $funcParams
                        { & $commandName @params } | should throw 'error!'
                    }
                }
            }
        }
    }
        
    describe 'WriteLog' {
    
        $commandName = 'WriteLog'
        

        mock 'Get-Date' {
            'time'
        }

        mock 'Export-Csv'
    
        $parameterSets = @(
            @{
                FilePath = 'C:\log.csv'
                CSVIdentifierValue = 'username'
                CSVIdentifierField = 'employeeid'
                Attributes = @{ 
                    ADAttributeName = 'EmployeeId'
                    ADAttributeValue = $null
                    CSVAttributeName = 'PERSON_NUM'
                    CSVAttributeValue = 123
                }
                TestName = 'Standard'
            }
        )
    
        $testCases = @{
            All = $parameterSets
        }
    
        it 'should export a CSV to the expected path: <TestName>' -TestCases $testCases.All {
            param($FilePath,$CSVIdentifierValue,$CSVIdentifierField,$Attributes)
        
            & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'Export-Csv'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { $PSBoundParameters.Path -eq $FilePath }
            }
            Assert-MockCalled @assMParams
        }

        it 'should appends to the CSV: <TestName>' -TestCases $testCases.All {
            param($FilePath,$CSVIdentifierValue,$CSVIdentifierField,$Attributes)
        
            & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'Export-Csv'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { $Append }
            }
            Assert-MockCalled @assMParams
        }

        it 'should export as CSV with the expected values: <TestName>' -TestCases $testCases.All {
            param($FilePath,$CSVIdentifierValue,$CSVIdentifierField,$Attributes)
        
            & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'Export-Csv'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { 
                    $InputObject.Time -eq 'time' -and
                    $InputObject.CSVIdentifierValue -eq $CSVIdentifierValue -and
                    $InputObject.CSVIdentifierField -eq $CSVIdentifierField -and
                    $InputObject.ADAttributeName -eq 'EmployeeId' -and
                    $InputObject.ADAttributeValue -eq $null -and
                    $InputObject.CSVAttributeName -eq 'PERSON_NUM' -and
                    $InputObject.CSVAttributeValue -eq 123
                }
            }
            Assert-MockCalled @assMParams
        }
    }

    describe 'Invoke-AdSync' {
    
        $commandName = 'Invoke-AdSync'
    
        #region Mocks
            $script:testAdUser = New-MockObject -Type 'System.DirectoryServices.AccountManagement.UserPrincipal'
                $amParams = @{
                    MemberType = 'NoteProperty'
                    Force = $true
                }
                $props = @{
                    'Name' = 'nameval'
                    'Enabled' = $true
                    'SamAccountName' = 'samval'
                    'GivenName' = 'givennameval'
                    'Surname' = 'surnameval'
                    'DisplayName' = 'displaynameval'
                    'OtherProperty' = 'otherval'
                    'EmployeeId' = 1
                    'Title' = 'titleval'
                }
                $props.GetEnumerator() | ForEach-Object {
                    $script:testAdUser | Add-Member @amParams -Name $_.Key -Value $_.Value
                }
                $script:testAdUser

            mock 'WriteLog'
            
            mock 'Test-Path' {
                $true
            }

            mock 'SyncCompanyUser'

            mock 'Write-Warning'

            mock 'TestCsvHeaderExists' {
                $true
            }

            mock 'Get-CompanyAdUser' {
                $script:allAdsiUsers
            }

            mock 'Get-CompanyCsvUser' {
                [pscustomobject]@{ 
                    AD_LOGON = "nameval"
                    PERSON_NUM = "1"
                    SyncAttrib1 = 'sync1'
                    SyncAttrib2 = 'sync2'
                }
            }

            mock 'FindUserMatch'

            mock 'GetCsvIdField' {
                [pscustomobject]@{
                    Field = 'x'
                    Value = 'y'
                }
            }
        #endregion

        $parameterSets = @(
            @{
                Label = 'ReportOnly'
                Parameters = @{
                    CsvFilePath = 'C:\log.csv'
                    FieldSyncMap = @{ 'csvfield1' = 'adfield1' }
                    FieldMatchMap = @{ PERSON_NUM = 'EmployeeId' }
                    ReportOnly = $true
                }
            }
            @{
                Label = 'Single sync /single match field'
                Parameters = @{
                    CsvFilePath = 'C:\log.csv'
                    FieldSyncMap = @{ 'csvfield1' = 'adfield1' }
                    FieldMatchMap = @{ PERSON_NUM = 'EmployeeId' }
                }
            }
            @{
                Label = 'Multi sync/Multi match field'
                Parameters = @{
                    CsvFilePath = 'C:\log.csv'
                    FieldSyncMap = @{ 
                        'csvfield1' = 'adfield1'
                        'csvfield2' = 'adfield2'
                    }
                    FieldMatchMap = @{ 
                        PERSON_NUM = 'EmployeeId'
                        AD_LOGON = 'samAcountName'
                    }
                }
            }
            @{
                Label = 'Exclude'
                Parameters = @{
                    CsvFilePath = 'C:\log.csv'
                    FieldMatchMap = @{ 'csvmatchfield1' = 'admatchfield1' }
                    FieldSyncMap = @{ 'csvfield1' = 'adfield1' }
                    Exclude = @{ ExcludeCol = 'excludeme' }
                }
            }
        )

        $testCases = $parameterSets

        foreach ($testCase in $testCases) {

            $parameters = $testCase.Parameters

            context $testCase.Label {

                $result = & $commandName @parameters

                if ($parameters.ContainsKey('Exclude')) {

                    context 'when excluding a CSV column' {

                        $result = & $commandName @parameters

                        context 'when a header does not exist' {
                        
                            mock 'TestCsvHeaderExists' {
                                $false
                            }

                            it 'should throw an exception' {
                                $params = @{} + $parameters
                                { & $commandName @params } | should throw 'One or more CSV headers excluded with -Exclude do not exist in the CSV file'
                            }
                        
                        }

                        context 'when all headers exist' {
                        
                            mock 'TestCsvHeaderExists' {
                                $true
                            }

                            $result = & $commandName @parameters

                            it 'should pass Exclude to Get-CompanyCsvUser' {

                                $assMParams = @{
                                    CommandName = 'Get-CompanyCsvUser'
                                    Times = 1
                                    Exactly = $true
                                    ParameterFilter = { 
                                        $PSBoundParameters.Exclude.Keys -eq 'ExcludeCol' -and
                                        $PSBoundParameters.Exclude.Values -eq 'excludeme'
                                    }
                                }
                                Assert-MockCalled @assMParams
                                
                            }
                        }
                    }
                }

                context 'when no AD users are found' {
                    
                    mock 'Get-CompanyAdUser'

                    it 'should throw an exception' {
                    
                        $params = @{} + $parameters
                        { & $commandName @params } | should throw 'No AD users found'
                    }
                }

                context 'when no CSV users are found' {
                    
                    mock 'Get-CompanyCsvUser'

                    it 'should throw an exception' {
                    
                        $params = @{} + $parameters
                        { & $commandName @params } | should throw 'No CSV users found'
                    }
                    
                }

                context 'when at least one AD user and one CSV user is found' {

                    $result = & $commandName @parameters
                    
                    it 'should return nothing' {
                        $result | should benullorempty
                    }
                    
                    context 'when a user match cannot be found' {

                        mock 'FindUserMatch'
                    
                        context 'when no CSV ID fields can be found' {

                            mock 'GetCsvIdField'

                            it 'should throw an exception' {
                            
                                $params = @{} + $parameters
                                { & $commandName @params } | should throw 'No CSV ID fields were found'
                            }            
                        }

                        context 'when at least one CSV ID field can be found' {

                            context 'when a populated CSV ID field exists' {
                                
                                mock 'GetCsvIdField' {
                                    [pscustomobject]@{
                                        Field = $null
                                        Value = 'val1'
                                    }
                                    [pscustomobject]@{
                                        Field = $null
                                        Value = 'val2'
                                    }
                                    [pscustomobject]@{
                                        Field = 'populatedfield1'
                                        Value = 'val1'
                                    }
                                }

                                $result = & $commandName @parameters
                                
                                it 'should pass the ID as the CSV id field for WriteLog' {

                                    $assMParams = @{
                                        CommandName = 'WriteLog'
                                        Times = 1
                                        Exactly = $true
                                        ParameterFilter = { 
                                            $PSBoundParameters.CSVIdentifierField -eq 'populatedfield1' 
                                        }
                                    }
                                    Assert-MockCalled @assMParams
                                }
                            
                            }

                            context 'when no CSV ID fields are populated' {
                            
                                mock 'GetCsvIdField' {
                                    [pscustomobject]@{
                                        Field = 'field1'
                                        Value = $null
                                    }
                                    [pscustomobject]@{
                                        Field = 'field2'
                                        Value = $null
                                    }
                                }

                                $result = & $commandName @parameters

                                it 'should display a warning' {
                                    $assMParams = @{
                                        CommandName = 'Write-Warning'
                                        Times = 1
                                        Exactly = $true
                                        ParameterFilter = { 
                                            $PSBoundParameters.Message -eq 'No CSV user identifier could be found' }
                                    }
                                    Assert-MockCalled @assMParams
                                }

                                it 'should pass N/A as the CSV id field for WriteLog' {

                                    $assMParams = @{
                                        CommandName = 'WriteLog'
                                        Times = 1
                                        Exactly = $true
                                        ParameterFilter = { 
                                            $PSBoundParameters.CsvIdentifierValue -eq 'N/A' -and
                                            $PSBoundParameters.CSVIdentifierField -eq 'field1,field2'
                                        }
                                    }
                                    Assert-MockCalled @assMParams
                                }
                            }
                        }
                    }

                    context 'when a user match can be found' {

                        mock 'FindUserMatch' {
                            [pscustomobject]@{
                                MatchedAdUser = $script:testAdUser
                                CsvIdMatchedOn = 'PERSON_NUM'
                                AdIdMatchedOn = 'EmployeeId'
                            }
                        }
                    
                        context 'when an attribute mismatch is found' {

                            mock 'FindAttributeMismatch' {
                                @{
                                    CSVAttributeName = 'x'
                                    CSVAttributeValue = 'y'
                                    ADAttributeName = 'z'
                                    ADAttributeValue = 'i'
                                }
                            }

                            if ($parameters.ContainsKey('ReportOnly')) {
                                context 'when only reporting' {

                                    $result = & $commandName @parameters

                                    it 'should not attempt to sync the user' {
                                    
                                        $assMParams = @{
                                            CommandName = 'SyncCompanyUser'
                                            Times = 0
                                            Exactly = $true
                                        }
                                        Assert-MockCalled @assMParams
                                    }
                                }
                            } else {
                                context 'when syncing' {

                                    $result = & $commandName @parameters

                                    it 'should sync the expected user' {

                                        $assMParams = @{
                                            CommandName = 'SyncCompanyUser'
                                            Times = 1
                                            Exactly = $true
                                            ParameterFilter = { 
                                                $PSBoundParameters.AdUser.Name -eq 'nameval' -and
                                                $PSBoundParameters.AdUser.Enabled -eq $true -and
                                                $PSBoundParameters.AdUser.SamAccountName -eq 'samval' -and
                                                $PSBoundParameters.AdUser.GivenName -eq 'givennameval' -and
                                                $PSBoundParameters.AdUser.Surname -eq 'surnameval' -and
                                                $PSBoundParameters.AdUser.DisplayName -eq 'displaynameval' -and
                                                $PSBoundParameters.AdUser.OtherProperty -eq 'otherval' -and
                                                $PSBoundParameters.AdUser.EmployeeId -eq 1 -and
                                                $PSBoundParameters.AdUser.Title -eq 'titleval' #-and
                                                $PSBoundParameters.CsvUser.AD_LOGON -eq 'nameval' -and
                                                $PSBoundParameters.CsvUser.PERSON_NUM -eq "1" -and
                                                $PSBoundParameters.CsvUser.SyncAttrib1 -eq 'sync1' -and
                                                $PSBoundParameters.CsvUser.SyncAttrib2 -eq 'sync2' -and
                                                $PSBoundParameters.Attributes.CSVAttributeName -eq 'x' -and
                                                $PSBoundParameters.Attributes.CSVAttributeValue -eq 'y' -and
                                                $PSBoundParameters.Attributes.ADAttributeName -eq 'z' -and
                                                $PSBoundParameters.Attributes.ADAttributeValue -eq 'i'
                                            }
                                        }
                                        Assert-MockCalled @assMParams
                                    }
                                }
                            }
                        }

                        context 'when all attributes are in sync' {

                            mock 'FindAttributeMismatch'

                            $result = & $commandName @parameters
                        
                            it 'should pass the expected attributes to WriteLog' {

                                    $assMParams = @{
                                        CommandName = 'WriteLog'
                                        Times = 1
                                        Exactly = $true
                                        ParameterFilter = { 
                                            $PSBoundParameters.Attributes.CSVAttributeName -eq 'AlreadyInSync' -and
                                            $PSBoundParameters.Attributes.CSVAttributeValue -eq 'AlreadyInSync' -and
                                            $PSBoundParameters.Attributes.ADAttributeName -eq 'AlreadyInSync' -and
                                            $PSBoundParameters.Attributes.ADAttributeValue -eq 'AlreadyInSync'
                                        }
                                    }
                                    Assert-MockCalled @assMParams
                                }
                        }
                    }
                }
            }
        }
    }

    Remove-Variable -Name allAdsiUsers -Scope Script
    Remove-Variable -Name allCsvUsers -Scope Script
}