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 } |