Tests/AD-UserLifecycle.Tests.ps1

BeforeAll {
    $modulePath = Split-Path -Parent $PSScriptRoot
    Import-Module "$modulePath\AD-UserLifecycle.psd1" -Force
}

Describe 'AD-UserLifecycle Module' {

    Context 'Module Loading' {
        It 'Should import without errors' {
            { Import-Module "$PSScriptRoot\..\AD-UserLifecycle.psd1" -Force } | Should -Not -Throw
        }

        It 'Should export exactly 3 public functions' {
            $commands = Get-Command -Module AD-UserLifecycle
            $commands.Count | Should -Be 3
        }

        It 'Should export New-ADUserFromTemplate' {
            Get-Command -Module AD-UserLifecycle -Name New-ADUserFromTemplate | Should -Not -BeNullOrEmpty
        }

        It 'Should export Disable-DepartedUser' {
            Get-Command -Module AD-UserLifecycle -Name Disable-DepartedUser | Should -Not -BeNullOrEmpty
        }

        It 'Should export Export-ADUserReport' {
            Get-Command -Module AD-UserLifecycle -Name Export-ADUserReport | Should -Not -BeNullOrEmpty
        }

        It 'Should not export private functions' {
            { Get-Command -Module AD-UserLifecycle -Name _New-RandomPassword -ErrorAction Stop } | Should -Throw
        }
    }

    Context 'New-ADUserFromTemplate Parameter Validation' {
        It 'Should have mandatory FirstName parameter in Single set' {
            (Get-Command New-ADUserFromTemplate).Parameters['FirstName'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should have mandatory LastName parameter in Single set' {
            (Get-Command New-ADUserFromTemplate).Parameters['LastName'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should have mandatory Template parameter in Single set' {
            (Get-Command New-ADUserFromTemplate).Parameters['Template'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should have mandatory CsvPath parameter in Bulk set' {
            (Get-Command New-ADUserFromTemplate).Parameters['CsvPath'].ParameterSets['Bulk'].IsMandatory | Should -BeTrue
        }

        It 'Should support -WhatIf' {
            (Get-Command New-ADUserFromTemplate).Parameters.ContainsKey('WhatIf') | Should -BeTrue
        }

        It 'Should have two parameter sets: Single and Bulk' {
            $cmd = Get-Command New-ADUserFromTemplate
            $cmd.ParameterSets.Name | Should -Contain 'Single'
            $cmd.ParameterSets.Name | Should -Contain 'Bulk'
        }

        It 'Should default to Single parameter set' {
            (Get-Command New-ADUserFromTemplate).DefaultParameterSet | Should -Be 'Single'
        }

        It 'Should validate CsvPath exists' {
            $validateScript = (Get-Command New-ADUserFromTemplate).Parameters['CsvPath'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidateScriptAttribute] }
            $validateScript | Should -Not -BeNullOrEmpty
        }
    }

    Context 'New-ADUserFromTemplate Mocked Execution' {
        BeforeAll {
            # Mock AD cmdlets to test logic without a domain
            Mock -ModuleName AD-UserLifecycle Import-Module { }
            Mock -ModuleName AD-UserLifecycle Get-ADUser {
                if ($Identity -eq 'Template.IT') {
                    [PSCustomObject]@{
                        DistinguishedName = 'CN=Template.IT,OU=Templates,OU=IT,DC=contoso,DC=com'
                        MemberOf          = @('CN=IT-Staff,OU=Groups,DC=contoso,DC=com')
                        StreetAddress     = '123 Main St'
                        City              = 'Seattle'
                        State             = 'WA'
                        PostalCode        = '98101'
                        Office            = 'HQ'
                        Company           = 'Contoso'
                    }
                }
                elseif ($Filter) { $null }  # SAMAccountName uniqueness check
            }
            Mock -ModuleName AD-UserLifecycle Get-ADDomain {
                [PSCustomObject]@{ DNSRoot = 'contoso.com' }
            }
            Mock -ModuleName AD-UserLifecycle New-ADUser { }
            Mock -ModuleName AD-UserLifecycle Add-ADGroupMember { }
            Mock -ModuleName AD-UserLifecycle Start-Transcript { }
            Mock -ModuleName AD-UserLifecycle Stop-Transcript { }
        }

        It 'Should call New-ADUser with correct display name format' {
            New-ADUserFromTemplate -FirstName 'Jane' -LastName 'Smith' -Template 'Template.IT' -EnableAccount -Confirm:$false

            Should -Invoke -CommandName New-ADUser -ModuleName AD-UserLifecycle -ParameterFilter {
                $Name -eq 'Smith, Jane' -and $DisplayName -eq 'Smith, Jane'
            }
        }

        It 'Should generate SAMAccountName as lastnamefirstinitial' {
            New-ADUserFromTemplate -FirstName 'Jane' -LastName 'Smith' -Template 'Template.IT' -EnableAccount -Confirm:$false

            Should -Invoke -CommandName New-ADUser -ModuleName AD-UserLifecycle -ParameterFilter {
                $SAMAccountName -eq 'smithj'
            }
        }

        It 'Should copy group memberships from template' {
            New-ADUserFromTemplate -FirstName 'Jane' -LastName 'Smith' -Template 'Template.IT' -EnableAccount -Confirm:$false

            Should -Invoke -CommandName Add-ADGroupMember -ModuleName AD-UserLifecycle -Times 1
        }

        It 'Should return result object with Created status' {
            $result = New-ADUserFromTemplate -FirstName 'Jane' -LastName 'Smith' -Template 'Template.IT' -EnableAccount -Confirm:$false

            $result.Status | Should -Be 'Created'
            $result.SAMAccountName | Should -Be 'smithj'
        }
    }

    Context 'Disable-DepartedUser Parameter Validation' {
        It 'Should have mandatory Identity parameter' {
            (Get-Command Disable-DepartedUser).Parameters['Identity'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should accept pipeline input for Identity' {
            (Get-Command Disable-DepartedUser).Parameters['Identity'].Attributes.ValueFromPipeline | Should -Contain $true
        }

        It 'Should support -WhatIf' {
            (Get-Command Disable-DepartedUser).Parameters.ContainsKey('WhatIf') | Should -BeTrue
        }

        It 'Should have High ConfirmImpact' {
            $cmdletBinding = (Get-Command Disable-DepartedUser).ScriptBlock.Attributes |
                Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] }
            $cmdletBinding.ConfirmImpact | Should -Be 'High'
        }

        It 'Should accept SAMAccountName as alias for Identity' {
            (Get-Command Disable-DepartedUser).Parameters['Identity'].Aliases | Should -Contain 'SAMAccountName'
        }
    }

    Context 'Disable-DepartedUser Mocked Execution' {
        BeforeAll {
            Mock -ModuleName AD-UserLifecycle Import-Module { }
            Mock -ModuleName AD-UserLifecycle Get-ADUser {
                [PSCustomObject]@{
                    SAMAccountName    = 'jsmith'
                    GivenName         = 'Jane'
                    Surname           = 'Smith'
                    DistinguishedName = 'CN=Jane Smith,OU=IT,DC=contoso,DC=com'
                    HomeDirectory     = $null
                    MemberOf          = @(
                        'CN=IT-Staff,OU=Groups,DC=contoso,DC=com',
                        'CN=VPN-Users,OU=Groups,DC=contoso,DC=com'
                    )
                    Description       = 'Systems Administrator'
                }
            }
            Mock -ModuleName AD-UserLifecycle Get-ADGroup { [PSCustomObject]@{ Name = 'IT-Staff' } }
            Mock -ModuleName AD-UserLifecycle Disable-ADAccount { }
            Mock -ModuleName AD-UserLifecycle Set-ADAccountPassword { }
            Mock -ModuleName AD-UserLifecycle Remove-ADGroupMember { }
            Mock -ModuleName AD-UserLifecycle Set-ADUser { }
            Mock -ModuleName AD-UserLifecycle Move-ADObject { }
            Mock -ModuleName AD-UserLifecycle Start-Transcript { }
            Mock -ModuleName AD-UserLifecycle Stop-Transcript { }
        }

        It 'Should disable the account' {
            Disable-DepartedUser -Identity 'jsmith' -DisabledOU 'OU=Disabled,DC=contoso,DC=com' -SkipHomeFolder -Confirm:$false

            Should -Invoke -CommandName Disable-ADAccount -ModuleName AD-UserLifecycle -Times 1
        }

        It 'Should reset the password' {
            Disable-DepartedUser -Identity 'jsmith' -DisabledOU 'OU=Disabled,DC=contoso,DC=com' -SkipHomeFolder -Confirm:$false

            Should -Invoke -CommandName Set-ADAccountPassword -ModuleName AD-UserLifecycle -Times 1
        }

        It 'Should remove from all groups' {
            Disable-DepartedUser -Identity 'jsmith' -DisabledOU 'OU=Disabled,DC=contoso,DC=com' -SkipHomeFolder -Confirm:$false

            Should -Invoke -CommandName Remove-ADGroupMember -ModuleName AD-UserLifecycle -Times 2
        }

        It 'Should move to the Disabled OU' {
            Disable-DepartedUser -Identity 'jsmith' -DisabledOU 'OU=Disabled,DC=contoso,DC=com' -SkipHomeFolder -Confirm:$false

            Should -Invoke -CommandName Move-ADObject -ModuleName AD-UserLifecycle -ParameterFilter {
                $TargetPath -eq 'OU=Disabled,DC=contoso,DC=com'
            }
        }

        It 'Should return Offboarded status' {
            $result = Disable-DepartedUser -Identity 'jsmith' -DisabledOU 'OU=Disabled,DC=contoso,DC=com' -SkipHomeFolder -Confirm:$false

            $result.Status | Should -Be 'Offboarded'
        }
    }

    Context 'Export-ADUserReport Parameter Validation' {
        It 'Should default OutputFormat to HTML' {
            (Get-Command Export-ADUserReport).Parameters['OutputFormat'].ParameterSets.Values.HelpMessage | Should -BeNullOrEmpty
        }

        It 'Should validate OutputFormat values' {
            $validateSet = (Get-Command Export-ADUserReport).Parameters['OutputFormat'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet.ValidValues | Should -Contain 'HTML'
            $validateSet.ValidValues | Should -Contain 'CSV'
            $validateSet.ValidValues | Should -Contain 'Both'
        }

        It 'Should default DaysInactive to 90' {
            (Get-Command Export-ADUserReport).Parameters.ContainsKey('DaysInactive') | Should -BeTrue
        }
    }

    Context '_New-RandomPassword' {
        BeforeAll {
            # Access private function via module scope
            $pwFunc = & (Get-Module AD-UserLifecycle) { Get-Command _New-RandomPassword }
        }

        It 'Should return a SecureString' {
            $result = & (Get-Module AD-UserLifecycle) { _New-RandomPassword }
            $result | Should -BeOfType [System.Security.SecureString]
        }

        It 'Should generate password of specified length' {
            $result = & (Get-Module AD-UserLifecycle) { _New-RandomPassword -Length 20 }
            $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($result)
            )
            $plain.Length | Should -Be 20
        }

        It 'Should include uppercase, lowercase, digits, and special characters' {
            $result = & (Get-Module AD-UserLifecycle) { _New-RandomPassword -Length 32 }
            $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($result)
            )
            $plain | Should -Match '[A-Z]'
            $plain | Should -Match '[a-z]'
            $plain | Should -Match '[0-9]'
            $plain | Should -Match '[!@#$%&*?]'
        }
    }
}