Tests/GPO-HealthAudit.Tests.ps1

#Requires -Modules Pester

<#
.SYNOPSIS
    Pester tests for GPO-HealthAudit module.
 
.DESCRIPTION
    Validates module loading, exported functions, parameter constraints, mock-based
    detection logic, and manifest correctness for the GPO-HealthAudit module.
#>


BeforeAll {
    $ModuleRoot = Split-Path -Path $PSScriptRoot -Parent
    $ManifestPath = Join-Path -Path $ModuleRoot -ChildPath 'GPO-HealthAudit.psd1'

    # Remove module if already loaded, then import fresh
    Get-Module -Name GPO-HealthAudit -ErrorAction SilentlyContinue | Remove-Module -Force
    Import-Module $ManifestPath -Force -ErrorAction Stop
}

Describe 'Module Loading' {
    It 'Should import the module without errors' {
        $Module = Get-Module -Name GPO-HealthAudit
        $Module | Should -Not -BeNullOrEmpty
        $Module.Name | Should -Be 'GPO-HealthAudit'
    }

    It 'Should export exactly 5 public functions' {
        $ExportedFunctions = (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys
        $ExportedFunctions.Count | Should -Be 5
    }

    It 'Should export Invoke-GPOHealthAudit' {
        (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys | Should -Contain 'Invoke-GPOHealthAudit'
    }

    It 'Should export Get-UnlinkedGPOs' {
        (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys | Should -Contain 'Get-UnlinkedGPOs'
    }

    It 'Should export Get-EmptyGPOs' {
        (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys | Should -Contain 'Get-EmptyGPOs'
    }

    It 'Should export Get-GPOPermissionReport' {
        (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys | Should -Contain 'Get-GPOPermissionReport'
    }

    It 'Should export Get-StaleGPOs' {
        (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys | Should -Contain 'Get-StaleGPOs'
    }

    It 'Should not export private functions' {
        (Get-Module -Name GPO-HealthAudit).ExportedFunctions.Keys | Should -Not -Contain 'New-HtmlDashboard'
    }
}

Describe 'Manifest Validation' {
    BeforeAll {
        $Manifest = Test-ModuleManifest -Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'GPO-HealthAudit.psd1') -ErrorAction Stop
    }

    It 'Should have a valid module manifest' {
        $Manifest | Should -Not -BeNullOrEmpty
    }

    It 'Should have the correct GUID' {
        $Manifest.Guid.ToString() | Should -Be 'e3f9a4b2-6c58-4d8e-bf23-0a4d7e9c8f56'
    }

    It 'Should require PowerShell 5.1' {
        $Manifest.PowerShellVersion.ToString() | Should -Be '5.1'
    }

    It 'Should have the correct author' {
        $Manifest.Author | Should -Be 'Larry Roberts, Independent Consultant'
    }

    It 'Should have a description' {
        $Manifest.Description | Should -Not -BeNullOrEmpty
        $Manifest.Description | Should -Match 'Group Policy'
    }

    It 'Should have correct tags' {
        $Tags = $Manifest.PrivateData.PSData.Tags
        $Tags | Should -Contain 'GroupPolicy'
        $Tags | Should -Contain 'GPO'
        $Tags | Should -Contain 'ActiveDirectory'
        $Tags | Should -Contain 'Audit'
    }

    It 'Should have a project URI' {
        $Manifest.PrivateData.PSData.ProjectUri | Should -Match 'GPO-HealthAudit'
    }
}

Describe 'Parameter Validation' {
    Context 'Get-StaleGPOs -DaysStale' {
        It 'Should accept DaysStale of 30' {
            $Cmd = Get-Command -Name Get-StaleGPOs
            $Param = $Cmd.Parameters['DaysStale']
            $ValidateRange = $Param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $ValidateRange.MinRange | Should -Be 30
        }

        It 'Should accept DaysStale up to 3650' {
            $Cmd = Get-Command -Name Get-StaleGPOs
            $Param = $Cmd.Parameters['DaysStale']
            $ValidateRange = $Param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $ValidateRange.MaxRange | Should -Be 3650
        }

        It 'Should default DaysStale to 365' {
            $Cmd = Get-Command -Name Get-StaleGPOs
            $Param = $Cmd.Parameters['DaysStale']
            $Param.DefaultValue | Should -Be 365
        }
    }

    Context 'Invoke-GPOHealthAudit -DaysStale' {
        It 'Should accept DaysStale range 30-3650' {
            $Cmd = Get-Command -Name Invoke-GPOHealthAudit
            $Param = $Cmd.Parameters['DaysStale']
            $ValidateRange = $Param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $ValidateRange.MinRange | Should -Be 30
            $ValidateRange.MaxRange | Should -Be 3650
        }
    }

    Context 'Invoke-GPOHealthAudit -OutputPath' {
        It 'Should have an OutputPath parameter' {
            $Cmd = Get-Command -Name Invoke-GPOHealthAudit
            $Cmd.Parameters.Keys | Should -Contain 'OutputPath'
        }

        It 'Should have a ValidateScript on OutputPath' {
            $Cmd = Get-Command -Name Invoke-GPOHealthAudit
            $Param = $Cmd.Parameters['OutputPath']
            $HasValidateScript = $Param.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateScriptAttribute] }
            $HasValidateScript | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Get-EmptyGPOs -IncludeDisabledSections' {
        It 'Should have an IncludeDisabledSections switch parameter' {
            $Cmd = Get-Command -Name Get-EmptyGPOs
            $Param = $Cmd.Parameters['IncludeDisabledSections']
            $Param.ParameterType.Name | Should -Be 'SwitchParameter'
        }
    }
}

Describe 'Get-UnlinkedGPOs Detection Logic' {
    BeforeAll {
        # Mock GPO XML report with NO LinksTo element (unlinked)
        $UnlinkedGpoXml = @'
<?xml version="1.0" encoding="utf-8"?>
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <Name>Test Unlinked GPO</Name>
    <Identifier>
        <Identifier>{12345678-abcd-1234-abcd-123456789012}</Identifier>
    </Identifier>
    <Computer>
        <VersionDirectory>1</VersionDirectory>
        <VersionSysvol>1</VersionSysvol>
        <Enabled>true</Enabled>
    </Computer>
    <User>
        <VersionDirectory>1</VersionDirectory>
        <VersionSysvol>1</VersionSysvol>
        <Enabled>true</Enabled>
    </User>
</GPO>
'@


        # Mock GPO XML report WITH a LinksTo element (linked)
        $LinkedGpoXml = @'
<?xml version="1.0" encoding="utf-8"?>
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <Name>Test Linked GPO</Name>
    <Identifier>
        <Identifier>{87654321-dcba-4321-dcba-210987654321}</Identifier>
    </Identifier>
    <LinksTo>
        <SOMName>corp.local</SOMName>
        <SOMPath>dc=corp,dc=local</SOMPath>
        <Enabled>true</Enabled>
        <NoOverride>false</NoOverride>
    </LinksTo>
    <Computer>
        <VersionDirectory>1</VersionDirectory>
        <VersionSysvol>1</VersionSysvol>
        <Enabled>true</Enabled>
    </Computer>
    <User>
        <VersionDirectory>1</VersionDirectory>
        <VersionSysvol>1</VersionSysvol>
        <Enabled>true</Enabled>
    </User>
</GPO>
'@


        $MockGPOs = @(
            [PSCustomObject]@{
                DisplayName      = 'Test Unlinked GPO'
                Id               = [Guid]'12345678-abcd-1234-abcd-123456789012'
                CreationTime     = (Get-Date).AddDays(-400)
                ModificationTime = (Get-Date).AddDays(-300)
                Owner            = 'CORP\DomainAdmins'
                GpoStatus        = 'AllSettingsEnabled'
            }
            [PSCustomObject]@{
                DisplayName      = 'Test Linked GPO'
                Id               = [Guid]'87654321-dcba-4321-dcba-210987654321'
                CreationTime     = (Get-Date).AddDays(-200)
                ModificationTime = (Get-Date).AddDays(-50)
                Owner            = 'CORP\DomainAdmins'
                GpoStatus        = 'AllSettingsEnabled'
            }
        )

        Mock -ModuleName GPO-HealthAudit -CommandName Get-Module { return $true }
        Mock -ModuleName GPO-HealthAudit -CommandName Import-Module { }
        Mock -ModuleName GPO-HealthAudit -CommandName Get-GPO { return $MockGPOs }
        Mock -ModuleName GPO-HealthAudit -CommandName Get-GPOReport {
            param($Guid, $ReportType)
            if ($Guid -eq '12345678-abcd-1234-abcd-123456789012') {
                return $UnlinkedGpoXml
            }
            else {
                return $LinkedGpoXml
            }
        }
    }

    It 'Should detect the unlinked GPO' {
        $Results = @(Get-UnlinkedGPOs)
        $Results.Count | Should -Be 1
        $Results[0].DisplayName | Should -Be 'Test Unlinked GPO'
    }

    It 'Should mark unlinked GPOs with UNLINKED finding' {
        $Results = @(Get-UnlinkedGPOs)
        $Results[0].Finding | Should -Be 'UNLINKED'
    }

    It 'Should not report linked GPOs' {
        $Results = @(Get-UnlinkedGPOs)
        $Results | Where-Object { $_.DisplayName -eq 'Test Linked GPO' } | Should -BeNullOrEmpty
    }

    It 'Should include Owner in the output' {
        $Results = @(Get-UnlinkedGPOs)
        $Results[0].Owner | Should -Not -BeNullOrEmpty
    }
}

Describe 'Get-EmptyGPOs Detection Logic' {
    BeforeAll {
        # Mock GPO XML report with NO ExtensionData (empty GPO)
        $EmptyGpoXml = @'
<?xml version="1.0" encoding="utf-8"?>
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <Name>Empty Test GPO</Name>
    <Computer>
        <VersionDirectory>0</VersionDirectory>
        <VersionSysvol>0</VersionSysvol>
        <Enabled>true</Enabled>
    </Computer>
    <User>
        <VersionDirectory>0</VersionDirectory>
        <VersionSysvol>0</VersionSysvol>
        <Enabled>true</Enabled>
    </User>
</GPO>
'@


        # Mock GPO XML report WITH ExtensionData (configured GPO)
        $ConfiguredGpoXml = @'
<?xml version="1.0" encoding="utf-8"?>
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <Name>Configured Test GPO</Name>
    <Computer>
        <VersionDirectory>3</VersionDirectory>
        <VersionSysvol>3</VersionSysvol>
        <Enabled>true</Enabled>
        <ExtensionData>
            <Extension xsi:type="q1:RegistrySettings" xmlns:q1="http://www.microsoft.com/GroupPolicy/Settings/Registry">
                <q1:Policy>
                    <q1:Name>TestPolicy</q1:Name>
                    <q1:State>Enabled</q1:State>
                </q1:Policy>
            </Extension>
        </ExtensionData>
    </Computer>
    <User>
        <VersionDirectory>0</VersionDirectory>
        <VersionSysvol>0</VersionSysvol>
        <Enabled>true</Enabled>
    </User>
</GPO>
'@


        $MockGPOs = @(
            [PSCustomObject]@{
                DisplayName      = 'Empty Test GPO'
                Id               = [Guid]'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
                CreationTime     = (Get-Date).AddDays(-500)
                ModificationTime = (Get-Date).AddDays(-500)
                Owner            = 'CORP\DomainAdmins'
                GpoStatus        = 'AllSettingsEnabled'
            }
            [PSCustomObject]@{
                DisplayName      = 'Configured Test GPO'
                Id               = [Guid]'11111111-2222-3333-4444-555555555555'
                CreationTime     = (Get-Date).AddDays(-100)
                ModificationTime = (Get-Date).AddDays(-10)
                Owner            = 'CORP\DomainAdmins'
                GpoStatus        = 'AllSettingsEnabled'
            }
        )

        Mock -ModuleName GPO-HealthAudit -CommandName Get-Module { return $true }
        Mock -ModuleName GPO-HealthAudit -CommandName Import-Module { }
        Mock -ModuleName GPO-HealthAudit -CommandName Get-GPO { return $MockGPOs }
        Mock -ModuleName GPO-HealthAudit -CommandName Get-GPOReport {
            param($Guid, $ReportType)
            if ($Guid -eq 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee') {
                return $EmptyGpoXml
            }
            else {
                return $ConfiguredGpoXml
            }
        }
    }

    It 'Should detect the empty GPO' {
        $Results = @(Get-EmptyGPOs)
        $Results.Count | Should -Be 1
        $Results[0].DisplayName | Should -Be 'Empty Test GPO'
    }

    It 'Should mark empty GPOs with EMPTY finding' {
        $Results = @(Get-EmptyGPOs)
        $Results[0].Finding | Should -Be 'EMPTY'
    }

    It 'Should report ComputerSettingsConfigured as false for empty GPO' {
        $Results = @(Get-EmptyGPOs)
        $Results[0].ComputerSettingsConfigured | Should -BeFalse
    }

    It 'Should report UserSettingsConfigured as false for empty GPO' {
        $Results = @(Get-EmptyGPOs)
        $Results[0].UserSettingsConfigured | Should -BeFalse
    }

    It 'Should not report configured GPOs as empty' {
        $Results = @(Get-EmptyGPOs)
        $Results | Where-Object { $_.DisplayName -eq 'Configured Test GPO' } | Should -BeNullOrEmpty
    }
}

Describe 'Get-StaleGPOs Detection Logic' {
    BeforeAll {
        $MockGPOs = @(
            [PSCustomObject]@{
                DisplayName      = 'Ancient GPO'
                Id               = [Guid]'99999999-8888-7777-6666-555555555555'
                CreationTime     = (Get-Date).AddDays(-1200)
                ModificationTime = (Get-Date).AddDays(-800)
                Owner            = 'CORP\DomainAdmins'
                GpoStatus        = 'AllSettingsEnabled'
            }
            [PSCustomObject]@{
                DisplayName      = 'Recent GPO'
                Id               = [Guid]'11111111-aaaa-bbbb-cccc-dddddddddddd'
                CreationTime     = (Get-Date).AddDays(-30)
                ModificationTime = (Get-Date).AddDays(-5)
                Owner            = 'CORP\DomainAdmins'
                GpoStatus        = 'AllSettingsEnabled'
            }
        )

        # Ancient GPO is unlinked; Recent GPO is linked
        $AncientGpoXml = @'
<?xml version="1.0" encoding="utf-8"?>
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings">
    <Name>Ancient GPO</Name>
    <Computer><Enabled>true</Enabled></Computer>
    <User><Enabled>true</Enabled></User>
</GPO>
'@


        $RecentGpoXml = @'
<?xml version="1.0" encoding="utf-8"?>
<GPO xmlns="http://www.microsoft.com/GroupPolicy/Settings">
    <Name>Recent GPO</Name>
    <LinksTo>
        <SOMName>corp.local/Workstations</SOMName>
        <SOMPath>ou=Workstations,dc=corp,dc=local</SOMPath>
        <Enabled>true</Enabled>
    </LinksTo>
    <Computer><Enabled>true</Enabled></Computer>
    <User><Enabled>true</Enabled></User>
</GPO>
'@


        Mock -ModuleName GPO-HealthAudit -CommandName Get-Module { return $true }
        Mock -ModuleName GPO-HealthAudit -CommandName Import-Module { }
        Mock -ModuleName GPO-HealthAudit -CommandName Get-GPO { return $MockGPOs }
        Mock -ModuleName GPO-HealthAudit -CommandName Get-GPOReport {
            param($Guid, $ReportType)
            if ($Guid -eq '99999999-8888-7777-6666-555555555555') {
                return $AncientGpoXml
            }
            else {
                return $RecentGpoXml
            }
        }
    }

    It 'Should detect the ancient GPO as stale' {
        $Results = @(Get-StaleGPOs -DaysStale 365)
        $Results | Where-Object { $_.DisplayName -eq 'Ancient GPO' } | Should -Not -BeNullOrEmpty
    }

    It 'Should not flag the recently modified GPO' {
        $Results = @(Get-StaleGPOs -DaysStale 365)
        $Results | Where-Object { $_.DisplayName -eq 'Recent GPO' } | Should -BeNullOrEmpty
    }

    It 'Should calculate DaysSinceModified correctly' {
        $Results = @(Get-StaleGPOs -DaysStale 365)
        $Stale = $Results | Where-Object { $_.DisplayName -eq 'Ancient GPO' }
        $Stale.DaysSinceModified | Should -BeGreaterThan 790
    }

    It 'Should mark unlinked stale GPOs as STALE_UNLINKED' {
        $Results = @(Get-StaleGPOs -DaysStale 365)
        $Stale = $Results | Where-Object { $_.DisplayName -eq 'Ancient GPO' }
        $Stale.Finding | Should -Be 'STALE_UNLINKED'
    }

    It 'Should respect the DaysStale parameter' {
        # With a very high threshold, nothing should be stale
        $Results = @(Get-StaleGPOs -DaysStale 3650)
        $Results | Where-Object { $_.DisplayName -eq 'Ancient GPO' } | Should -BeNullOrEmpty
    }

    It 'Should report IsLinked as false for unlinked GPOs' {
        $Results = @(Get-StaleGPOs -DaysStale 365)
        $Stale = $Results | Where-Object { $_.DisplayName -eq 'Ancient GPO' }
        $Stale.IsLinked | Should -BeFalse
    }
}

AfterAll {
    Get-Module -Name GPO-HealthAudit -ErrorAction SilentlyContinue | Remove-Module -Force
}