tests/UnipharLeaversManagement.Tests.ps1
|
<#
.SYNOPSIS Pester tests for the destructive Entra ID / AD helper functions in UnipharLeaversManagement. .DESCRIPTION Covers the cloud-facing leaver functions via mocked Microsoft Graph / Exchange / AD cmdlets: Disable-EntraUserAccount, Hide-EntraUserFromGAL, Reset-EntraUserPassword, Remove-EntraAuthenticationMethods, plus -WhatIf (ShouldProcess) gating for Convert-MailboxToSharedWithOOO and Move-OnPremUserToDisabledOU. These are consumed by the Invoke-DisableLeavers runbook. .NOTES Author: Security Team Requires: Pester v5 #> BeforeAll { $securityAuthPath = Join-Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) 'UnipharSecurityAuth/UnipharSecurityAuth.psm1' $leaversPath = Join-Path (Split-Path $PSScriptRoot -Parent) 'UnipharLeaversManagement.psm1' # UnipharLeaversManagement depends on New-RandomPassword / Protect-LdapFilterValue. Import-Module $securityAuthPath -Force Import-Module $leaversPath -Force # Stub the external Graph / Exchange / AD cmdlets (global scope) so Mock -ModuleName can # resolve them without the real SDKs installed. function global:Get-MgUser { param($UserId, $Property) } function global:Update-MgUser { param($UserId, $AccountEnabled, $ShowInAddressList, $PasswordProfile, $OnPremisesExtensionAttributes) } function global:Get-Mailbox { param($Identity) } function global:Set-Mailbox { param($Identity, $Type) } function global:Get-ADUser { param($Filter, $Server, $Properties, [PSCredential]$Credential) } function global:Set-ADUser { param($Identity, $Replace, $Server, [PSCredential]$Credential) } function global:Move-ADObject { param($Identity, $TargetPath, $Server, [PSCredential]$Credential) } function global:Get-MgUserAuthenticationPhoneMethod { param($UserId) } function global:Remove-MgUserAuthenticationPhoneMethod { param($UserId, $PhoneAuthenticationMethodId) } function global:Get-MgUserAuthenticationEmailMethod { param($UserId) } function global:Remove-MgUserAuthenticationEmailMethod { param($UserId, $EmailAuthenticationMethodId) } function global:Get-MgUserAuthenticationFido2Method { param($UserId) } function global:Remove-MgUserAuthenticationFido2Method { param($UserId, $Fido2AuthenticationMethodId) } function global:Get-MgUserAuthenticationMicrosoftAuthenticatorMethod { param($UserId) } function global:Remove-MgUserAuthenticationMicrosoftAuthenticatorMethod { param($UserId, $MicrosoftAuthenticatorAuthenticationMethodId) } function global:Get-MgUserAuthenticationSoftwareOathMethod { param($UserId) } function global:Remove-MgUserAuthenticationSoftwareOathMethod { param($UserId, $SoftwareOathAuthenticationMethodId) } function global:Get-MgUserAuthenticationTemporaryAccessPassMethod { param($UserId) } function global:Remove-MgUserAuthenticationTemporaryAccessPassMethod { param($UserId, $TemporaryAccessPassAuthenticationMethodId) } function global:Get-MgUserAuthenticationWindowsHelloForBusinessMethod { param($UserId) } function global:Remove-MgUserAuthenticationWindowsHelloForBusinessMethod { param($UserId, $WindowsHelloForBusinessAuthenticationMethodId) } function global:Get-MgUserAuthenticationPasswordlessMicrosoftAuthenticatorMethod { param($UserId) } function global:Remove-MgUserAuthenticationPasswordlessMicrosoftAuthenticatorMethod { param($UserId, $PasswordlessMicrosoftAuthenticatorAuthenticationMethodId) } } AfterAll { Get-ChildItem function:global:*-Mg*, function:global:*-Mailbox, function:global:*-ADUser, function:global:Move-ADObject -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item "function:global:$($_.Name)" -ErrorAction SilentlyContinue } Remove-Module UnipharLeaversManagement -Force -ErrorAction SilentlyContinue Remove-Module UnipharSecurityAuth -Force -ErrorAction SilentlyContinue } Describe 'Disable-EntraUserAccount' -Tags 'Unit' { It 'Disables the account via Update-MgUser with AccountEnabled false' { Mock -ModuleName UnipharLeaversManagement Update-MgUser { } $result = Disable-EntraUserAccount -UserPrincipalName 'leaver@uniphar.ie' $result.Disabled | Should -BeTrue Should -Invoke -ModuleName UnipharLeaversManagement Update-MgUser -Times 1 -ParameterFilter { $UserId -eq 'leaver@uniphar.ie' -and $AccountEnabled -eq $false } } It 'Reports failure without throwing when Graph errors' { Mock -ModuleName UnipharLeaversManagement Update-MgUser { throw 'Graph down' } $result = Disable-EntraUserAccount -UserPrincipalName 'leaver@uniphar.ie' $result.Disabled | Should -BeFalse $result.Error | Should -Match 'Graph down' } } Describe 'Hide-EntraUserFromGAL' -Tags 'Unit' { It 'Hides the user via Update-MgUser with ShowInAddressList false' { Mock -ModuleName UnipharLeaversManagement Update-MgUser { } $result = Hide-EntraUserFromGAL -UserPrincipalName 'leaver@uniphar.ie' $result.HiddenFromGAL | Should -BeTrue Should -Invoke -ModuleName UnipharLeaversManagement Update-MgUser -Times 1 -ParameterFilter { $ShowInAddressList -eq $false } } } Describe 'Reset-EntraUserPassword' -Tags 'Unit' { It 'Sets a 90-character password that does not force change at next sign-in' { Mock -ModuleName UnipharLeaversManagement Get-MgUser { [pscustomobject]@{ Id = '00000000-0000-0000-0000-000000000001' } } Mock -ModuleName UnipharLeaversManagement Update-MgUser { } $result = Reset-EntraUserPassword -UserPrincipalName 'leaver@uniphar.ie' $result.PasswordReset | Should -BeTrue Should -Invoke -ModuleName UnipharLeaversManagement Update-MgUser -Times 1 -ParameterFilter { $PasswordProfile.password.Length -eq 90 -and $PasswordProfile.forceChangePasswordNextSignIn -eq $false } } It 'Returns UserNotFound when the user does not exist' { Mock -ModuleName UnipharLeaversManagement Get-MgUser { $null } Mock -ModuleName UnipharLeaversManagement Update-MgUser { } $result = Reset-EntraUserPassword -UserPrincipalName 'ghost@uniphar.ie' $result.PasswordReset | Should -BeFalse $result.Error | Should -Be 'UserNotFound' Should -Invoke -ModuleName UnipharLeaversManagement Update-MgUser -Times 0 } } Describe 'Remove-EntraAuthenticationMethods' -Tags 'Unit' { It 'Removes a registered phone method' { Mock -ModuleName UnipharLeaversManagement Get-MgUser { [pscustomobject]@{ Id = '00000000-0000-0000-0000-000000000001' } } Mock -ModuleName UnipharLeaversManagement Get-MgUserAuthenticationPhoneMethod { @([pscustomobject]@{ Id = 'phone-1'; PhoneType = 'mobile'; PhoneNumber = '+353861234567' }) } Mock -ModuleName UnipharLeaversManagement Remove-MgUserAuthenticationPhoneMethod { } $result = Remove-EntraAuthenticationMethods -UserPrincipalName 'leaver@uniphar.ie' $result.MethodsRemoved | Should -Contain 'Phone' Should -Invoke -ModuleName UnipharLeaversManagement Remove-MgUserAuthenticationPhoneMethod -Times 1 -ParameterFilter { $PhoneAuthenticationMethodId -eq 'phone-1' } } } Describe 'Convert-MailboxToSharedWithOOO (ShouldProcess gating)' -Tags 'Unit' { It 'Does not call Set-Mailbox under -WhatIf' { Mock -ModuleName UnipharLeaversManagement Get-Mailbox { [pscustomobject]@{ RecipientTypeDetails = 'UserMailbox' } } Mock -ModuleName UnipharLeaversManagement Set-Mailbox { } $result = Convert-MailboxToSharedWithOOO -UserPrincipalName 'leaver@uniphar.ie' -WhatIf $result.ConversionResult | Should -Be 'WhatIf-WouldConvert' Should -Invoke -ModuleName UnipharLeaversManagement Set-Mailbox -Times 0 } It 'Converts to shared for a normal user mailbox' { Mock -ModuleName UnipharLeaversManagement Get-Mailbox { [pscustomobject]@{ RecipientTypeDetails = 'UserMailbox' } } Mock -ModuleName UnipharLeaversManagement Set-Mailbox { } $result = Convert-MailboxToSharedWithOOO -UserPrincipalName 'leaver@uniphar.ie' $result.ConversionResult | Should -Be 'ConvertedToShared' Should -Invoke -ModuleName UnipharLeaversManagement Set-Mailbox -Times 1 -ParameterFilter { $Type -eq 'Shared' } } It 'Skips conversion when the mailbox is already shared' { Mock -ModuleName UnipharLeaversManagement Get-Mailbox { [pscustomobject]@{ RecipientTypeDetails = 'SharedMailbox' } } Mock -ModuleName UnipharLeaversManagement Set-Mailbox { } $result = Convert-MailboxToSharedWithOOO -UserPrincipalName 'leaver@uniphar.ie' $result.ConversionResult | Should -Be 'AlreadyShared' Should -Invoke -ModuleName UnipharLeaversManagement Set-Mailbox -Times 0 } } Describe 'Move-OnPremUserToDisabledOU (ShouldProcess gating)' -Tags 'Unit' { It 'Does not call Move-ADObject under -WhatIf' { Mock -ModuleName UnipharLeaversManagement Get-ADUser { [pscustomobject]@{ DistinguishedName = 'CN=Test User,OU=Users,DC=uniphar,DC=local' } } Mock -ModuleName UnipharLeaversManagement Set-ADUser { } Mock -ModuleName UnipharLeaversManagement Move-ADObject { } $result = Move-OnPremUserToDisabledOU -UserPrincipalName 'leaver@uniphar.ie' -Server 'dc.uniphar.local' -WhatIf $result.Moved | Should -BeFalse $result.Error | Should -Be 'WhatIf' Should -Invoke -ModuleName UnipharLeaversManagement Move-ADObject -Times 0 } It 'Moves the user to the disabled OU when confirmed' { Mock -ModuleName UnipharLeaversManagement Get-ADUser { [pscustomobject]@{ DistinguishedName = 'CN=Test User,OU=Users,DC=uniphar,DC=local' } } Mock -ModuleName UnipharLeaversManagement Set-ADUser { } Mock -ModuleName UnipharLeaversManagement Move-ADObject { } $result = Move-OnPremUserToDisabledOU -UserPrincipalName 'leaver@uniphar.ie' -Server 'dc.uniphar.local' -DisabledOU 'OU=Disabled,DC=uniphar,DC=local' $result.Moved | Should -BeTrue Should -Invoke -ModuleName UnipharLeaversManagement Move-ADObject -Times 1 -ParameterFilter { $TargetPath -eq 'OU=Disabled,DC=uniphar,DC=local' } } It 'Skips the move when the user is already in the disabled OU' { Mock -ModuleName UnipharLeaversManagement Get-ADUser { [pscustomobject]@{ DistinguishedName = 'CN=Test User,OU=Disabled,DC=uniphar,DC=local' } } Mock -ModuleName UnipharLeaversManagement Move-ADObject { } $result = Move-OnPremUserToDisabledOU -UserPrincipalName 'leaver@uniphar.ie' -Server 'dc.uniphar.local' -DisabledOU 'OU=Disabled,DC=uniphar,DC=local' $result.Error | Should -Be 'AlreadyInDisabledOU' Should -Invoke -ModuleName UnipharLeaversManagement Move-ADObject -Times 0 } } Describe 'Merge-LeaverGroupBackupValue (private)' -Tags 'Unit' { It 'Tags new group names with the source and merges with existing values' { InModuleScope UnipharLeaversManagement { $result = Merge-LeaverGroupBackupValue -Existing @('AD:Existing Group') -GroupNames @('Sales', 'Ops') -Source 'AAD' $result | Should -Contain 'AD:Existing Group' $result | Should -Contain 'AAD:Sales' $result | Should -Contain 'AAD:Ops' } } It 'Deduplicates and drops blank or null group names (idempotent re-run)' { InModuleScope UnipharLeaversManagement { $result = Merge-LeaverGroupBackupValue -Existing @('AD:Ops') -GroupNames @('Ops', '', $null, 'Ops') -Source 'AD' @($result).Count | Should -Be 1 $result | Should -Contain 'AD:Ops' } } } Describe 'Merge-LeaverInfoField (private)' -Tags 'Unit' { It 'Adds a key line when info is empty' { InModuleScope UnipharLeaversManagement { Merge-LeaverInfoField -Existing $null -Key 'OriginalOU' -Value 'OU=Users,DC=uniphar,DC=local' | Should -Be 'OriginalOU=OU=Users,DC=uniphar,DC=local' } } It 'Replaces the target key and preserves other keys' { InModuleScope UnipharLeaversManagement { $existing = "OriginalOU=OU=Old,DC=uniphar,DC=local`nLicenses=ENTERPRISEPACK" $result = Merge-LeaverInfoField -Existing $existing -Key 'OriginalOU' -Value 'OU=New,DC=uniphar,DC=local' $result | Should -Match 'OriginalOU=OU=New,DC=uniphar,DC=local' $result | Should -Match 'Licenses=ENTERPRISEPACK' @($result -split "`n").Count | Should -Be 2 } } } |