tests/UnipharSecurityAuthHelpers.Tests.ps1

<#
.SYNOPSIS
    Pester tests for the Workday synchronization helper functions in UnipharSecurityAuth.
 
.DESCRIPTION
    Covers the pure helper functions used by the Invoke-WorkdayUserSync runbook:
    ConvertTo-E164PhoneNumber, Get-CleanManagerName, Get-UserAttributeDiff, and Get-OrphanedEmployeeId.
 
.NOTES
    Author: Security Team
    Requires: Pester v5
#>


BeforeAll {
    $modulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'UnipharSecurityAuth.psm1'
    Import-Module $modulePath -Force
}

AfterAll {
    Remove-Module UnipharSecurityAuth -Force -ErrorAction SilentlyContinue
}

Describe 'ConvertTo-E164PhoneNumber' -Tags 'Unit' {
    Context 'When the value is a valid international number' {
        It 'Normalizes "<Value>" to "<Expected>"' -ForEach @(
            @{ Value = '+353 (86) 1687760'; Expected = '+353 861687760' }
            @{ Value = '+44 (7801) 930577'; Expected = '+44 7801930577' }
            @{ Value = '+1 (619) 3792154'; Expected = '+1 6193792154' }
            @{ Value = '+31 (6) 84790375'; Expected = '+31 684790375' }
            @{ Value = '+966 (54) 0055577'; Expected = '+966 540055577' }
            @{ Value = '+353 861687760'; Expected = '+353 861687760' }
            @{ Value = ' +353 86 168 7760 '; Expected = '+353 861687760' }
        ) {
            ConvertTo-E164PhoneNumber -PhoneNumber $Value | Should -BeExactly $Expected
        }
    }

    Context 'When the value has a trailing extension' {
        It 'Preserves the extension for "<Value>"' -ForEach @(
            @{ Value = '+353 (89) 7050141 x1'; Expected = '+353 897050141x1' }
            @{ Value = '+1 (619) 3792154 ext 123'; Expected = '+1 6193792154x123' }
            @{ Value = '+1 (619) 3792154 extension 5'; Expected = '+1 6193792154x5' }
        ) {
            ConvertTo-E164PhoneNumber -PhoneNumber $Value | Should -BeExactly $Expected
        }
    }

    Context 'When the value cannot be normalized' {
        It 'Returns $null for "<Value>"' -ForEach @(
            @{ Value = $null }
            @{ Value = '' }
            @{ Value = ' ' }
            @{ Value = '0861687760' }
            @{ Value = '(086) 1687760' }
            @{ Value = '+353 12' }
        ) {
            ConvertTo-E164PhoneNumber -PhoneNumber $Value | Should -BeNullOrEmpty
        }
    }

    It 'Accepts pipeline input' {
        '+353 (86) 1687760' | ConvertTo-E164PhoneNumber | Should -BeExactly '+353 861687760'
    }
}

Describe 'Get-CleanManagerName' -Tags 'Unit' {
    Context 'When the name has a trailing parenthetical' {
        It 'Strips the parenthetical from "<Value>"' -ForEach @(
            @{ Value = 'Tara McGuinness (On Leave)'; Expected = 'Tara McGuinness' }
            @{ Value = 'John Smith (Contractor)'; Expected = 'John Smith' }
            @{ Value = 'Jane Doe (A) (B)'; Expected = 'Jane Doe' }
            @{ Value = 'Jane Doe (On Leave) '; Expected = 'Jane Doe' }
        ) {
            Get-CleanManagerName -ManagerName $Value | Should -BeExactly $Expected
        }
    }

    Context 'When the name has no parenthetical' {
        It 'Returns the trimmed name for "<Value>"' -ForEach @(
            @{ Value = 'ROBERTA KANCIAUSKAITE'; Expected = 'ROBERTA KANCIAUSKAITE' }
            @{ Value = ' Sarah Connor '; Expected = 'Sarah Connor' }
        ) {
            Get-CleanManagerName -ManagerName $Value | Should -BeExactly $Expected
        }
    }

    Context 'When the value is empty' {
        It 'Returns $null for "<Value>"' -ForEach @(
            @{ Value = $null }
            @{ Value = '' }
            @{ Value = ' ' }
            @{ Value = '(On Leave)' }
        ) {
            Get-CleanManagerName -ManagerName $Value | Should -BeNullOrEmpty
        }
    }
}

Describe 'Get-UserAttributeDiff' -Tags 'Unit' {
    It 'Returns only the changed attributes' {
        $current = @{ DisplayName = 'Jane Doe'; Department = 'Sales' }
        $desired = @{ DisplayName = 'Jane Doe'; Department = 'Marketing'; Company = 'Uniphar' }

        $diff = Get-UserAttributeDiff -Current $current -Desired $desired

        $diff.Keys | Should -HaveCount 2
        $diff['Department'] | Should -BeExactly 'Marketing'
        $diff['Company'] | Should -BeExactly 'Uniphar'
        $diff.ContainsKey('DisplayName') | Should -BeFalse
    }

    It 'Ignores case and surrounding whitespace when comparing' {
        $current = @{ Department = 'sales ' }
        $desired = @{ Department = 'SALES' }

        (Get-UserAttributeDiff -Current $current -Desired $desired).Keys | Should -HaveCount 0
    }

    It 'Never blanks an existing attribute from an empty desired value' {
        $current = @{ Department = 'Sales' }
        $desired = @{ Department = ''; Company = $null }

        (Get-UserAttributeDiff -Current $current -Desired $desired).Keys | Should -HaveCount 0
    }

    It 'Treats a missing current key as a change when desired is non-empty' {
        $current = @{}
        $desired = @{ Company = 'Uniphar' }

        $diff = Get-UserAttributeDiff -Current $current -Desired $desired
        $diff['Company'] | Should -BeExactly 'Uniphar'
    }

    It 'Handles a null current hashtable' {
        $desired = @{ Company = 'Uniphar' }
        $diff = Get-UserAttributeDiff -Current $null -Desired $desired
        $diff['Company'] | Should -BeExactly 'Uniphar'
    }

    It 'Returns an empty hashtable when nothing changed' {
        $current = @{ DisplayName = 'Jane Doe' }
        $desired = @{ DisplayName = 'Jane Doe' }
        (Get-UserAttributeDiff -Current $current -Desired $desired).Keys | Should -HaveCount 0
    }
}

Describe 'Get-OrphanedEmployeeId' -Tags 'Unit' {
    It 'Returns current IDs that are not in the valid set' {
        $result = Get-OrphanedEmployeeId -CurrentEmployeeIds @('100', '200', '300') -ValidEmployeeIds @('100', '300')
        $result | Should -Be @('200')
    }

    It 'Compares case-insensitively and ignores whitespace' {
        $result = Get-OrphanedEmployeeId -CurrentEmployeeIds @(' ABC ', 'def') -ValidEmployeeIds @('abc')
        $result | Should -Be @('def')
    }

    It 'De-duplicates orphaned IDs' {
        $result = Get-OrphanedEmployeeId -CurrentEmployeeIds @('200', '200', '201') -ValidEmployeeIds @('100')
        $result | Should -Be @('200', '201')
    }

    It 'Skips null and empty current IDs' {
        $result = Get-OrphanedEmployeeId -CurrentEmployeeIds @('', $null, ' ', '999') -ValidEmployeeIds @('100')
        $result | Should -Be @('999')
    }

    It 'Returns an empty array when all current IDs are valid' {
        $result = @(Get-OrphanedEmployeeId -CurrentEmployeeIds @('100', '200') -ValidEmployeeIds @('100', '200'))
        $result | Should -HaveCount 0
    }

    It 'Returns all current IDs when the valid set is empty' {
        $result = Get-OrphanedEmployeeId -CurrentEmployeeIds @('100', '200') -ValidEmployeeIds @()
        $result | Should -Be @('100', '200')
    }
}