PSADSync.Tests.ps1

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

Import-Module -Name $ThisModule,'ADSIPS' -Force -ErrorAction Stop

$global:allAdsiUsers = 0..10 | foreach {
    $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 {
        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
}

$global:allCsvUsers = 0..15 | foreach {
    $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 
}

InModuleScope $ThisModuleName {

    describe 'Get-CompanyCsvUser' {
    
        $commandName = 'Get-CompanyCsvUser'
        $command = Get-Command -Name $commandName
    
        #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 { 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)
            
                $result = & $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

            (diff $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

            (diff @('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

            (diff @('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'
            diff $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'
        $command = Get-Command -Name $commandName
    
        #region Mocks
            mock 'Get-AdsiUser' {
                $global:allAdsiUsers | where { $_.Enabled }
            } -ParameterFilter { $LdapFilter }

            mock 'Get-AdsiUser' {
                $global:allAdsiUsers
            } -ParameterFilter { -not $LdapFilter }
        #endregion
        
        $parameterSets = @(
            @{
                TestName = 'Only enabled users'
            }
            @{
                All = $true
                TestName = 'All users'
            }
        )
    
        $testCases = @{
            All = $parameterSets
            AllUsers = $parameterSets.where({$_.ContainsKey('All')})
            EnabledUsers = $parameterSets.where({-not $_.ContainsKey('All')})
        }

        it 'when All is used, it returns all users: <TestName>' -TestCases $testCases.AllUsers {
            param($All,$Credential)
        
            $result = & $commandName @PSBoundParameters
            @($result).Count | should be $global:allAdsiUsers.Count
        }

        it 'when All is not used, it returns only enabled users: <TestName>' -TestCases $testCases.EnabledUsers {
            param($All,$Credential)
        
            $result = & $commandName @PSBoundParameters
            @($result).Count | should be ($global:allAdsiUsers | where { $_.Enabled }).Count
        }

    }

    describe 'CompareCompanyUser' {
    
        $commandName = 'CompareCompanyUser'
        $command = Get-Command -Name $commandName
    
        #region Mocks
            mock 'FindUserMatch' {
                [pscustomobject]@{
                    MatchedAdUser = ($global:allAdsiUsers | where EmployeeId -eq '10' )
                    IdMatchedOn = 'AD_LOGON'
                }
            } -ParameterFilter { $CsvUser.AD_LOGON -eq 'nameval0' }

            mock 'FindUserMatch' {
                [pscustomobject]@{
                    MatchedAdUser = ($global:allAdsiUsers | where EmployeeId -eq '11' )
                    IdMatchedOn = 'AD_LOGON'
                }
            } -ParameterFilter { $CsvUser.AD_LOGON -eq 'nameval1' }

            mock 'FindUserMatch' {
                [pscustomobject]@{
                    MatchedAdUser = ($global:allAdsiUsers | where EmployeeId -eq '12')
                    IdMatchedOn = 'AD_LOGON'
                }
            } -ParameterFilter { $CsvUser.AD_LOGON -eq 'nameval2' }

            mock 'FindUserMatch' {

            } -ParameterFilter { $CsvUser.AD_LOGON -eq 'nameval11' }
        #endregion
        
        $parameterSets = @(
            @{
                AdUsers = $global:allAdsiUsers
                CsvUsers = $global:allCsvUsers
                TestName = 'Default'
            }
        )
    
        $testCases = @{
            All = $parameterSets
        }

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

        it 'should return the expected object properties: <TestName>' -TestCases $testCases.All {
            param($AdUsers,$CsvUsers)
        
            $result = & $commandName @PSBoundParameters
            @($result).foreach({
                $_.PSObject.Properties.Name -contains 'CsvUser' | should be $true
                $_.PSObject.Properties.Name -contains 'AdUser' | should be $true
                $_.PSObject.Properties.Name -contains 'Match' | should be $true
            })
        }

        it 'should return the expected object type: <TestName>' -TestCases $testCases.All {
            param($AdUsers,$CsvUsers)
        
            $result = & $commandName @PSBoundParameters
            $result | should beoftype 'pscustomobject'
        }

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

            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval0'})).AdUser.EmployeeId | should be '10'
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval0'})).Match | should be $true
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval1'})).AdUser.EmployeeId | should be '11'
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval1'})).Match | should be $true
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval2'})).AdUser.EmployeeId | should be '12'
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval2'})).Match | should be $true
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval11'})).AdUser | should benullorempty
            (@($result).where({ $_.CsvUser.AD_LOGON -eq 'nameval11'})).Match | should be $false

        }

        context 'when FindUserMatch sends a break statement' {

            mock 'FindUserMatch' {
                foreach ($i in 0..1) { break }
            }
        
            it 'should return the expected number of objects: <TestName>' -TestCases $testCases.All {
                param($AdUsers,$CsvUsers)
            
                $result = & $commandName @PSBoundParameters
                @($result).Count | should be $global:allCsvUsers.Count
            }
            
        
        }

        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($AdUsers,$CsvUsers)
            
                $params = @{} + $PSBoundParameters
                { & $commandName @params } | should throw 'error!'
            }
        }
    }

    describe 'FindUserMatch' {
    
        $commandName = 'FindUserMatch'
        $command = Get-Command -Name $commandName
    
        #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]@{
                    AD_LOGON = $null
                    PERSON_NUM = 111
                }
            )

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

            $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
                TestName = 'Match on 1 ID'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:csvUserMatchOnAllIdentifers
                TestName = 'Match on all IDs'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:csvUserNoMatch
                TestName = 'No Match'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:OneblankCsvUserIdentifier
                TestName = 'One Blank ID'
            }
            @{
                AdUsers = $script:AdUsers
                CsvUser = $script:AllblankCsvUserIdentifier
                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({ -not $_.CsvUser.AD_LOGON -and ($_.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)
            
                & $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)
            
                $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)
            
                $result = & $commandName @PSBoundParameters

                $result.MatchedAdUser.EmployeeId | should be 123
                $result.IdMatchedOn = 'AD_LOGON'

            }
        }

        context 'When multiple matches could be found' {

            it 'should return the expected number of objects: <TestName>' -TestCases $testCases.MatchOnAllIds {
                param($AdUsers,$CsvUser)
            
                $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)
            
                $result = & $commandName @PSBoundParameters

                $result.MatchedAdUser.EmployeeId | should be 123
                $result.IdMatchedOn = 'AD_LOGON'

            }
        }

        context 'when one identifer is blank' {

            it 'should do nothing: <TestName>' -TestCases $testCases.OneBlankId {
                param($AdUsers,$CsvUser)
            
                $result = & $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)
            
                $result = & $commandName @PSBoundParameters
                $result.MatchedAdUser.EmployeeId | should be 111
                $result.IdMatchedOn = 'PERSON_NUM'
            }

        }

        context 'when all identifers are blank' {

            it 'should do nothing: <TestName>' -TestCases $testCases.AllBlankIds {
                param($AdUsers,$CsvUser)
            
                $result = & $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.OneBlankId {
                param($AdUsers,$CsvUser)
            
                $result = & $commandName @PSBoundParameters
                @($result.MatchedAdUser).foreach({
                    $_.PSObject.Properties.Name -contains 'EmployeeId' | should be $true
                })
                $result.IdMatchedOn = 'AD_LOGON'
            }
        
        }
    }

    describe 'FindAttributeMismatch' {
    
        $commandName = 'FindAttributeMismatch'
        $command = Get-Command -Name $commandName
    
        #region Mocks
            mock 'Write-Verbose'

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

            $script:csvUserNoMisMatch = [pscustomobject]@{
                AD_LOGON = 'foo'
                PERSON_NUM = 1111
                OtherAtrrib = '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 -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 -PassThru

            mock 'Get-Member' {
                [pscustomobject]@{
                    Name = 'samAccountName'
                }
                [pscustomobject]@{
                    Name = 'EmployeeId'
                }
            }
        #endregion
        
        $parameterSets = @(
            @{
                AdUser = $script:AdUserMisMatch
                CsvUser = $script:csvUserMisMatch
                TestName = 'Mismatch'
            }
            @{
                AdUser = $script:AdUserNoMisMatch
                CsvUser = $script:csvUserNoMisMatch
                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)
        
            $result = & $commandName @PSBoundParameters

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

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

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

            Assert-MockCalled @assMParams
        }

        context 'when a mismatch is found' {

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

        context 'when no mismatches are found' {

            it 'should return nothing: <TestName>' -TestCases $testCases.NoMismatch {
                param($AdUser,$CsvUser)
            
                & $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)
            
                $params = @{} + $PSBoundParameters
                { & $commandName @params } | should throw 'error!'
            }
        }
    
    }

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

        mock 'Set-AdUser'

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

        $script:csvUser = [pscustomobject]@{
            AD_LOGON = 'foo'
            PERSON_NUM = 123
            OtherAtrrib = 'x'
        }
    
        $parameterSets = @(
            @{
                AdUser = $script:AdUser
                CsvUser = $script:csvUser
                Attributes = [pscustomobject]@{ 
                    ADAttributeName = 'EmployeeId'
                    ADAttributeValue = $null
                    CSVAttributeName = 'PERSON_NUM'
                    CSVAttributeValue = 123
                 }
                Identifier = 'samAccountName'
                TestName = 'Standard'
            }
        )
    
        $testCases = @{
            All = $parameterSets
        }
    
        it 'should change only those attributes in the Attributes parameter: <TestName>' -TestCases $testCases.All {
            param($AdUser,$CsvUser,$Identifier,$Attributes,$Credential,$DomainController)
        
            $result = & $commandName @PSBoundParameters -Confirm:$false

            $assMParams = @{
                CommandName = 'Set-AdUser'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = {
                    ($Replace.EmployeeId -eq 123) -and
                    (-not ($Replace.GetEnumerator() | where { $_.Key -ne 'EmployeeId'}))
                 }
            }
            Assert-MockCalled @assMParams
        }

        it 'should change attributes on the expected user account: <TestName>' -TestCases $testCases.All {
            param($AdUser,$CsvUser,$Identifier,$Attributes,$Credential,$DomainController)
        
            $result = & $commandName @PSBoundParameters -Confirm:$false

            $assMParams = @{
                CommandName = 'Set-AdUser'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { [string]$Identity -eq 'foo' }
            }
            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: <TestName>' -TestCases $testCases.All {
                param($AdUser,$CsvUser,$Identifier,$Attributes,$Credential,$DomainController)
            
                $params = @{} + $PSBoundParameters
                { & $commandName @params -Confirm:$false } | should throw 'error!'
            }
        }
    }
        
    describe 'WriteLog' {
    
        $commandName = 'WriteLog'
        $command = Get-Command -Name $commandName

        mock 'Get-Date' {
            'time'
        }

        mock 'Export-Csv'
    
        $parameterSets = @(
            @{
                FilePath = 'C:\log.csv'
                CSVIdentifierValue = 'username'
                CSVIdentifierField = 'employeeid'
                Attributes = [pscustomobject]@{ 
                    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)
        
            $result = & $commandName @PSBoundParameters

            $assMParams = @{
                CommandName = 'Export-Csv'
                Times = 1
                Exactly = $true
                Scope = 'It'
                ParameterFilter = { $Path -eq 'C:\log.csv' }
            }
            Assert-MockCalled @assMParams
        }

        it 'should appends to the CSV: <TestName>' -TestCases $testCases.All {
            param($FilePath,$CSVIdentifierValue,$CSVIdentifierField,$Attributes)
        
            $result = & $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)
        
            $result = & $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'
        $command = Get-Command -Name $commandName

        #region Mocks

            mock 'Get-CompanyAdUser' {
                $global:allAdsiUsers | where { $_.Enabled }
            }

            mock 'Get-CompanyCsvUser' {
                $global:allCsvUsers
            } -ParameterFilter { -not $Exclude }

            mock 'Get-CompanyCsvUser' {
                $global:allCsvUsers | where { $_.ExcludeCol -ne 'excludeme' }
            } -ParameterFilter { $Exclude }

            mock 'CompareCompanyUser' {
                [pscustomobject]@{
                    CSVUser = [pscustomobject]@{
                        AD_LOGON = 'nameval0'
                        PERSON_NUM = '10'
                    }
                    ADUser = ($global:allAdsiUsers | where EmployeeId -eq '10' )
                    Match = $true
                    IdMatchedOn = 'AD_LOGON'
                }
                [pscustomobject]@{
                    CSVUser = [pscustomobject]@{
                        AD_LOGON = 'nameval15'
                        PERSON_NUM = '11'
                    }
                    ADUser = $null
                    IdMatchedOn = $null
                    Match = $false
                }
            }

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

            mock 'SyncCompanyUser'

            mock 'Write-Warning'

            mock 'TestCsvHeaderExists' {
                $true
            }
        #endregion
        
    
        $parameterSets = @(
            @{
                CsvFilePath = 'C:\log.csv'
                TestName = 'Sync'
            }
            @{
                CsvFilePath = 'C:\log.csv'
                ReportOnly = $true
                TestName = 'Report'
            }
            @{
                CsvFilePath = 'C:\log.csv'
                Exclude = @{ ExcludeCol = 'excludeme' }
                TestName = 'Excluded valid col'
            }
            @{
                CsvFilePath = 'C:\log.csv'
                Exclude = @{ ColNotHere = 'excludeme' }
                TestName = 'Exclude bogus col'
            }
        )
    
        $testCases = @{
            All = $parameterSets
            ReportOnly = $parameterSets.where({$_.ContainsKey('ReportOnly')})
            Sync = $parameterSets.where({-not $_.ContainsKey('ReportOnly')})
            NoExclusions = $parameterSets.where({-not $_.ContainsKey('Exclude')})
            ExcludeCol = $parameterSets.where({$_.ContainsKey('Exclude') -and (-not $_.Exclude.Keys.Contains('ColNotHere'))})
            ExcludeBogusCol = $parameterSets.where({$_.ContainsKey('Exclude') -and ($_.Exclude.Keys.Contains('ColNotHere'))})
        }

        context 'when a column is attempted to be excluded does not exist' {

            mock 'TestCsvHeaderExists' {
                $false
            }

            it 'should throw an exception: <TestName>' -TestCases $testCases.ExcludeBogusCol {
                param($CsvFilePath,$ReportOnly,$Exclude)
            
                $params = @{} + $PSBoundParameters
                { & $commandName @params } | should throw 'One or more CSV headers excluded with -Exclude do not exist in the CSV file'
            }

            it 'should stop execution: <TestName>' -TestCases $testCases.ExcludeBogusCol {
                param($CsvFilePath,$ReportOnly,$Exclude)
            
                try { $result = & $commandName @PSBoundParameters } catch {}

                $assMParams = @{
                    CommandName = 'CompareCompanyUser'
                    Times = 0
                    Exactly = $true
                    Scope = 'It'
                }
                Assert-MockCalled @assMParams
            }
        
        }

        it 'should return nothing: <TestName>' -TestCases $testCases.All {
            param($CsvFilePath,$ReportOnly,$Exclude)
        
            & $commandName @PSBoundParameters | should benullorempty
        }

        context 'when a null ID field is encountered in the CSV' {
        
            mock 'CompareCompanyUser' {
                [pscustomobject]@{
                    CSVUser = [pscustomobject]@{
                        AD_LOGON = 'nameval0'
                        PERSON_NUM = '10'
                    }
                    ADUser = ($global:allAdsiUsers | where EmployeeId -eq '10' )
                    IDMatchedon = 'AD_LOGON'
                    Match = $true
                }
                [pscustomobject]@{
                    CSVUser = [pscustomobject]@{
                        AD_LOGON = $null
                        PERSON_NUM = $null
                    }
                    ADUser = $null
                    IDMatchedon = $null
                    Match = $false
                }
            }

            it 'should write a warning: <TestName>' -TestCases $testCases.All {
                param($CsvFilePath,$ReportOnly,$Exclude)
            
                $result = & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'Write-Warning'
                    Times = 1
                    Exactly = $true
                    Scope = 'It'
                    ParameterFilter = { $Message -match 'The CSV user identifier field' }
                }
                Assert-MockCalled @assMParams
            }

        
        }

        context 'When no user can be matched' {
            
            mock 'CompareCompanyUser' {
                [pscustomobject]@{
                    CSVUser = [pscustomobject]@{
                        AD_LOGON = 'foo'
                        PERSON_NUM = 'x'
                    }
                    ADUser = $null
                    IDMatchedOn = $null
                    Match = $false
                }
            }

            it 'should write the expected contents to the log file: <TestName>' -TestCases $testCases.All {
            param($CsvFilePath,$ReportOnly,$Exclude)
            
                $result = & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'WriteLog'
                    Times = 1
                    Exactly = $true
                    Scope = 'It'
                    ParameterFilter = { 
                        $PSBoundParameters.CSVIdentifierField -eq 'AD_LOGON,PERSON_NUM' -and
                        $PSBoundParameters.CSVIdentifierValue -eq 'foo,x' -and
                        $PSBoundParameters.Attributes.CSVAttributeName -eq 'NoMatch' -and
                        $PSBoundParameters.Attributes.CSVAttributeValue -eq 'NoMatch' -and
                        $PSBoundParameters.Attributes.ADAttributeName -eq 'NoMatch' -and
                        $PSBoundParameters.Attributes.ADAttributeValue -eq 'NoMatch'
                    }
                }
                Assert-MockCalled @assMParams
            }

        }

        context 'when AD is already in sync' {

            mock 'CompareCompanyUser' {
                [pscustomobject]@{
                    CSVUser = [pscustomobject]@{
                        AD_LOGON = 'nameval2'
                        PERSON_NUM = '12'
                    }
                    ADUser = ($global:allAdsiUsers | where EmployeeId -eq '12' )
                    Match = $true
                    IdMatchedOn = 'AD_LOGON'
                }
            }

            mock 'FindAttributeMismatch'

            it 'should write the expected contents to the log file: <TestName>' -TestCases $testCases.All {
                param($CsvFilePath,$ReportOnly,$Exclude)
            
                $result = & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'WriteLog'
                    Times = 1
                    Exactly = $true
                    Scope = 'It'
                    ParameterFilter = { 
                        $IdentifierValu3 -eq 'foo' -and
                        $CSVIdentifierField -eq 'EmployeeId'
                        $Attributes.CSVAttributeName -eq 'AlreadyInSync' -and
                        $Attributes.CSVAttributeValue -eq 'AlreadyInSync' -and
                        $Attributes.ADAttributeName -eq 'AlreadyInSync' -and
                        $Attributes.ADAttributeValue -eq 'AlreadyInSync'
                    }
                }
                Assert-MockCalled @assMParams
            }

        }

        context 'when only reporting' {

            it 'should not attempt to sync the user: <TestName>' -TestCases $testCases.ReportOnly {
                param($CsvFilePath,$ReportOnly,$Exclude)
            
                $result = & $commandName @PSBoundParameters

                $assMParams = @{
                    CommandName = 'SyncCompanyUser'
                    Times = 0
                }
                Assert-MockCalled @assMParams
            }

        }

        context 'when an exception is thrown' {

            mock 'CompareCompanyUser' {
                throw 'error!'
            }

            it 'should return a non-terminating error: <TestName>' -TestCases $testCases.All {
                param($CsvFilePath,$ReportOnly,$Exclude)
            
                try { $null = & $commandName @PSBoundParameters -ErrorAction SilentlyContinue -ErrorVariable err } catch {}
                $err | should not benullorempty
            }
        }
    }
}

Remove-Variable -Name allAdsiUsers -Scope 'Global'
Remove-Variable -Name allCsvUsers -Scope 'Global'