Testing/Unit/PowerShell/CyberConfig/CyberConfig.Tests.ps1

using module '..\..\..\..\Modules\CyberConfig\CyberConfig.psm1'

Describe "CyberConfig Module Unit Tests" {
    BeforeAll {
        # Create a global mock for ConvertFrom-Yaml to avoid needing powershell-yaml module in CI/CD
        function global:ConvertFrom-Yaml {
            param($Yaml)
            
            # Simple mock that returns a hashtable (not PSCustomObject)
            # Handle both array and string inputs
            $Content = if ($Yaml -is [array]) { $Yaml -join "`n" } else { $Yaml }
            
            # Parse basic YAML syntax like "ProductNames: [aad]" or "ProductNames: [aad, exo]"
            if ($Content -match 'ProductNames:\s*\[([^\]]+)\]') {
                $ProductsString = $matches[1]
                $Products = $ProductsString -split ',' | ForEach-Object { $_.Trim() }
                return @{
                    ProductNames = $Products
                }
            }
            
            # Return hashtable with ProductNames if found on separate line
            if ($Content -match 'ProductNames:') {
                return @{
                    ProductNames = @('aad')
                }
            }
            
            # Return hashtable with at least ProductNames to satisfy validation
            return @{
                ProductNames = @('aad')
            }
        }
    }
    
    AfterAll {
        # Clean up the global mock
        Remove-Item -Path Function:\ConvertFrom-Yaml -ErrorAction SilentlyContinue
    }
    
    BeforeEach {
        # Reset the instance before each test to prevent state bleed
        [CyberConfig]::ResetInstance()
    }

    AfterEach {
        # Reset the instance after each test to prevent state bleed
        [CyberConfig]::ResetInstance()
    }

    AfterAll {
        # Clean up after tests
        [CyberConfig]::ResetInstance()
    }

    Context "Class Structure and Properties" {
        It "Should be a valid PowerShell class" {
            [CyberConfig] | Should -Not -BeNullOrEmpty
            [CyberConfig].Name | Should -Be "CyberConfig"
        }

        It "Should have required static properties" {
            # Check that the class has static members - the private properties aren't directly accessible
            [CyberConfig] | Get-Member -Static | Should -Not -BeNullOrEmpty
        }

        It "Should have required instance properties" {
            $Instance = [CyberConfig]::GetInstance()

            # Instance should be created successfully and be usable
            $Instance | Should -Not -BeNullOrEmpty
            $Instance.GetType().Name | Should -Be "CyberConfig"
        }

        It "Should have required static methods" {
            $StaticMethods = [CyberConfig] | Get-Member -Static -MemberType Method | Select-Object -ExpandProperty Name

            $StaticMethods | Should -Contain "GetInstance"
            $StaticMethods | Should -Contain "ResetInstance"
            $StaticMethods | Should -Contain "InitializeValidator"
            $StaticMethods | Should -Contain "CyberDefault"
            $StaticMethods | Should -Contain "GetConfigDefaults"
            $StaticMethods | Should -Contain "ValidateConfigFile"
            $StaticMethods | Should -Contain "GetSupportedProducts"
            $StaticMethods | Should -Contain "GetSupportedEnvironments"
            $StaticMethods | Should -Contain "GetProductInfo"
            $StaticMethods | Should -Contain "GetPrivilegedRoles"
        }

        It "Should have required instance methods" {
            $Instance = [CyberConfig]::GetInstance()
            $InstanceMethods = $Instance | Get-Member -MemberType Method | Select-Object -ExpandProperty Name

            $InstanceMethods | Should -Contain "LoadConfig"
            $InstanceMethods | Should -Contain "ValidateConfiguration"
        }
    }

    Context "Singleton Pattern Implementation" {
        It "Should return the same instance on multiple calls" {
            $Instance1 = [CyberConfig]::GetInstance()
            $Instance2 = [CyberConfig]::GetInstance()

            $Instance1 | Should -Be $Instance2
            $Instance1.GetHashCode() | Should -Be $Instance2.GetHashCode()
        }

        It "Should create new instance after reset" {
            # Load some configuration into the instance to create state
            $Instance1 = [CyberConfig]::GetInstance()
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                $Instance1.LoadConfig($TempFile)
                $HasConfig1 = $Instance1.Configuration -ne $null

                [CyberConfig]::ResetInstance()

                $Instance2 = [CyberConfig]::GetInstance()
                $HasConfig2 = $Instance2.Configuration -ne $null

                # After reset, new instance should not have the old configuration
                $HasConfig1 | Should -Be $true
                $HasConfig2 | Should -Be $false
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
            }
        }

        It "Should properly initialize on first access" {
            [CyberConfig]::ResetInstance()

            # Should not throw when getting instance
            { [CyberConfig]::GetInstance() } | Should -Not -Throw

            # Should have initialized the validator
            [CyberConfig]::_ValidatorInitialized | Should -Be $true
        }
    }

    Context "Static Method Functionality" {
        It "Should initialize validator without errors" {
            { [CyberConfig]::InitializeValidator() } | Should -Not -Throw
        }

        It "Should get configuration defaults" {
            $Defaults = [CyberConfig]::GetConfigDefaults()

            $Defaults | Should -Not -BeNullOrEmpty
            $Defaults | Should -BeOfType [PSCustomObject]
        }

        It "Should get supported products as array" {
            $Products = [CyberConfig]::GetSupportedProducts()

            $Products | Should -Not -BeNullOrEmpty
            # Ensure it's iterable (could be array or single value)
            $ProductsArray = @($Products)
            $ProductsArray.Count | Should -BeGreaterThan 0
        }

        It "Should get supported environments as array" {
            $Environments = [CyberConfig]::GetSupportedEnvironments()

            $Environments | Should -Not -BeNullOrEmpty
            # Ensure it's iterable (could be array or single value)
            $EnvironmentsArray = @($Environments)
            $EnvironmentsArray.Count | Should -BeGreaterThan 0
        }

        It "Should get product info for valid products" {
            $Products = [CyberConfig]::GetSupportedProducts()
            $FirstProduct = $Products[0]

            $ProductInfo = [CyberConfig]::GetProductInfo($FirstProduct)

            $ProductInfo | Should -Not -BeNullOrEmpty
            $ProductInfo | Should -BeOfType [PSCustomObject]
        }

        It "Should get privileged roles as array" {
            $Roles = [CyberConfig]::GetPrivilegedRoles()

            $Roles | Should -Not -BeNullOrEmpty
            # Ensure it's iterable (could be array or single value)
            $RolesArray = @($Roles)
            $RolesArray.Count | Should -BeGreaterThan 0
        }

        It "Should provide backward compatibility with CyberDefault method" {
            { [CyberConfig]::CyberDefault('DefaultOPAVersion') } | Should -Not -Throw
            { [CyberConfig]::CyberDefault('DefaultProductNames') } | Should -Not -Throw
            { [CyberConfig]::CyberDefault('DefaultM365Environment') } | Should -Not -Throw
        }

        It "Should validate configuration files" {
            # Create a minimal test file
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                $Result = [CyberConfig]::ValidateConfigFile($TempFile)
                $Result | Should -Not -BeNullOrEmpty
                $Result | Should -BeOfType [PSCustomObject]
                $Result.PSObject.Properties.Name | Should -Contain 'IsValid'
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
            }
        }
    }

    Context "Instance Method Functionality" {
        It "Should have empty configuration initially" {
            $Instance = [CyberConfig]::GetInstance()

            $Instance.Configuration | Should -BeNullOrEmpty
        }

        It "Should load configuration files" {
            $Instance = [CyberConfig]::GetInstance()

            # Create a minimal test file
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                { $Instance.LoadConfig($TempFile) } | Should -Not -Throw
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
            }
        }

        It "Should validate current configuration" {
            $Instance = [CyberConfig]::GetInstance()

            # Load some configuration first
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            
            # Create the default OPAPath directory so validation passes
            $DefaultOPAPath = Join-Path -Path $env:USERPROFILE -ChildPath ".cyberassessment\Tools"
            $OPAPathCreated = $false
            if (-not (Test-Path -Path $DefaultOPAPath)) {
                New-Item -Path $DefaultOPAPath -ItemType Directory -Force | Out-Null
                $OPAPathCreated = $true
            }
            
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                $Instance.LoadConfig($TempFile)
                # Method should exist and be callable
                { $Instance.ValidateConfiguration() } | Should -Not -Throw
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
                # Clean up the OPAPath directory if we created it
                if ($OPAPathCreated -and (Test-Path -Path $DefaultOPAPath)) {
                    Remove-Item -Path $DefaultOPAPath -Recurse -Force -ErrorAction SilentlyContinue
                }
            }
        }

        It "Should support skip validation parameter in LoadConfig" {
            $Instance = [CyberConfig]::GetInstance()

            # Create a minimal test file
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                { $Instance.LoadConfig($TempFile, $true) } | Should -Not -Throw
                { $Instance.LoadConfig($TempFile, $false) } | Should -Not -Throw
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
            }
        }
    }

    Context "Error Handling" {
        It "Should handle invalid file paths gracefully" {
            $Instance = [CyberConfig]::GetInstance()

            { $Instance.LoadConfig("nonexistent-file.yaml") } | Should -Throw
        }

        It "Should handle invalid product names in GetProductInfo" {
            { [CyberConfig]::GetProductInfo("InvalidProduct") } | Should -Not -Throw
        }

        It "Should handle invalid CyberDefault keys" {
            # Invalid keys should throw exceptions as designed
            { [CyberConfig]::CyberDefault("InvalidKey") } | Should -Throw
        }
    }

    Context "State Management" {
        It "Should maintain configuration state between calls" {
            $Instance = [CyberConfig]::GetInstance()

            # Create a test file
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                $Instance.LoadConfig($TempFile)
                $Instance.Configuration | Should -Not -BeNullOrEmpty

                # Get same instance and check configuration persists
                $SameInstance = [CyberConfig]::GetInstance()
                $SameInstance.Configuration | Should -Not -BeNullOrEmpty
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
            }
        }

        It "Should clear state on reset" {
            $Instance = [CyberConfig]::GetInstance()

            # Load some configuration
            $TempFile = [System.IO.Path]::ChangeExtension([System.IO.Path]::GetTempFileName(), '.yaml')
            "ProductNames: [aad]" | Set-Content -Path $TempFile

            try {
                $Instance.LoadConfig($TempFile)
                $Instance.Configuration | Should -Not -BeNullOrEmpty

                # Reset and get new instance
                [CyberConfig]::ResetInstance()
                $NewInstance = [CyberConfig]::GetInstance()
                $NewInstance.Configuration | Should -BeNullOrEmpty
            }
            finally {
                Remove-Item -Path $TempFile -Force -ErrorAction SilentlyContinue
            }
        }
    }
}