tests/Invoke-RemoteHardening.Tests.ps1

Describe "Invoke-RemoteHardening" {
    BeforeAll {
        # Load Core module first (contains Write-Log and other dependencies)
        $coreModulePath = (Resolve-Path "$PSScriptRoot\..\modules\Core.psm1").Path
        Import-Module $coreModulePath -Force -Scope Global

        # Load System module (contains Invoke-RemoteHardening and related functions)
        $systemModulePath = (Resolve-Path "$PSScriptRoot\..\modules\System.psm1").Path
        Import-Module $systemModulePath -Force -Scope Global
    }

    AfterAll {
        Remove-Module Core -Force -ErrorAction SilentlyContinue
        Remove-Module System -Force -ErrorAction SilentlyContinue
    }

    BeforeEach {
        # Mock Write-Log to prevent actual logging during tests
        Mock -CommandName Write-Log -MockWith { }
        Mock -CommandName Write-ErrorLog -MockWith { }
    }
    Context "Parameter Validation" {
        It "throws when ComputerName is empty" {
            { Invoke-RemoteHardening -ComputerName @() -Profile Basis } | Should -Throw
        }

        It "throws when Profile is invalid" {
            { Invoke-RemoteHardening -ComputerName 'localhost' -Profile InvalidProfile } | Should -Throw
        }

        It "accepts valid Profile values" {
            @('Basis', 'Recommended', 'Strict') | ForEach-Object {
                Mock New-PSSession { return @{ ComputerName = 'localhost' } }
                Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
                Mock Remove-PSSession { }
                Mock Write-Log { }

                # PSAvoidUsingComputerNameHardcoded suppressed: test fixture
                { Invoke-RemoteHardening -ComputerName 'localhost' -Profile $_ } | Should -Not -Throw
            }
        }
    }

    Context "Remote Session Creation" {
        It "creates PSSession with default port 5985" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } } -Verifiable
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis

            Should -InvokeVerifiable
        }

        It "creates PSSession with HTTPS when UseSSL specified" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } } -Verifiable -ParameterFilter { $UseSSL -eq $true }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -Port 5986 -UseSSL

            Should -InvokeVerifiable
        }

        It "throws when session creation fails" {
            Mock New-PSSession { return $null }
            Mock Write-ErrorLog { }

            # PSAvoidUsingComputerNameHardcoded suppressed: test fixture
            { Invoke-RemoteHardening -ComputerName 'invalid-host' -Profile Basis } | Should -Throw "Failed to establish remote sessions"
        }

        It "passes Credential parameter to New-PSSession" {
            # PSAvoidUsingConvertToSecureStringWithPlainText suppressed: test fixture with dummy credentials
            $credential = New-Object System.Management.Automation.PSCredential('TestUser', (ConvertTo-SecureString 'TestPass' -AsPlainText -Force))
            Mock New-PSSession { return @{ ComputerName = 'localhost' } } -Verifiable -ParameterFilter { $Credential -eq $credential }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            # PSAvoidUsingComputerNameHardcoded suppressed: test fixture
            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -Credential $credential

            Should -InvokeVerifiable
        }
    }

    Context "WhatIf Support" {
        It "respects WhatIf flag" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -WhatIf
            $result | Should -BeNullOrEmpty
        }

        It "executes without WhatIf" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis
            $result | Should -Not -BeNullOrEmpty
        }
    }

    Context "Execution Modes" {
        It "processes sequential by default" {
            Mock New-PSSession { return @(@{ ComputerName = 'Host1' }, @{ ComputerName = 'Host2' }) }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName @('Host1', 'Host2') -Profile Basis
            $result | Should -HaveCount 2
        }

        It "supports parallel execution" {
            Mock New-PSSession { return @(@{ ComputerName = 'Host1' }, @{ ComputerName = 'Host2' }) }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName @('Host1', 'Host2') -Profile Basis -Parallel
            $result | Should -HaveCount 2
        }
    }

    Context "Output Format" {
        It "returns PSCustomObject with required properties for success" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command {
                return @{
                    AppliedRules = @(1, 2, 3);
                    FailedRules = @();
                    ComplianceReport = @{
                        CompliancePercentage = 100;
                        Status = 'OK'
                    };
                    Duration = '00:00:05'
                }
            }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis

            $result | Should -Not -BeNullOrEmpty
            $result.ComputerName | Should -Be 'localhost'
            $result.Success | Should -Be $true
            $result.Profile | Should -Be 'Basis'
            $result.AppliedRules | Should -Be 3
            $result.FailedRules | Should -Be 0
            $result.CompliancePercentage | Should -Be 100
            $result.Status | Should -Be 'OK'
        }

        It "returns PSCustomObject with error properties on failure" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { throw [System.Exception]'Remote execution failed' }
            Mock Remove-PSSession { }
            Mock Write-Log { }
            Mock Write-ErrorLog { }

            $result = Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -ErrorAction SilentlyContinue

            $result | Should -Not -BeNullOrEmpty
            $result.ComputerName | Should -Be 'localhost'
            $result.Success | Should -Be $false
            $result.Error | Should -Match 'Remote execution failed'
        }
    }

    Context "Error Handling" {
        It "handles remote execution errors gracefully" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { throw [System.Exception]'Module not found' }
            Mock Remove-PSSession { }
            Mock Write-Log { }
            Mock Write-ErrorLog { }

            $result = Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -ErrorAction SilentlyContinue
            $result.Success | Should -Be $false
        }

        It "logs errors with Write-Log" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { throw [System.Exception]'Test error' }
            Mock Remove-PSSession { }
            Mock Write-Log { }
            Mock Write-ErrorLog { }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -ErrorAction SilentlyContinue

            Assert-MockCalled Write-Log -Scope It
        }

        It "throws on fatal errors when ErrorActionPreference is Stop" {
            Mock New-PSSession { return $null }
            Mock Write-ErrorLog { }

            # PSAvoidUsingComputerNameHardcoded suppressed: test fixture
            { Invoke-RemoteHardening -ComputerName 'invalid' -Profile Basis } | Should -Throw
        }
    }

    Context "SkipVerification Parameter" {
        It "passes SkipVerification to remote scriptblock" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } } -Verifiable
            Mock Remove-PSSession { }
            Mock Write-Log { }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -SkipVerification

            Should -InvokeVerifiable
        }
    }

    Context "Cleanup" {
        It "removes remote sessions after execution" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { } -Verifiable

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis

            Should -InvokeVerifiable
        }

        It "removes sessions even on error" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { throw [System.Exception]'Error' }
            Mock Remove-PSSession { } -Verifiable
            Mock Write-Log { }
            Mock Write-ErrorLog { }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis -ErrorAction SilentlyContinue

            Should -InvokeVerifiable
        }
    }

    Context "Logging" {
        It "logs start of remote hardening" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { } -Verifiable -ParameterFilter { $Message -match 'Starting remote hardening' }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis

            Should -InvokeVerifiable
        }

        It "logs successful hardening completion" {
            Mock New-PSSession { return @{ ComputerName = 'localhost' } }
            Mock Invoke-Command { return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { } -ParameterFilter { $Message -match 'Hardening succeeded' }

            Invoke-RemoteHardening -ComputerName 'localhost' -Profile Basis

            Assert-MockCalled Write-Log -ParameterFilter { $Message -match 'Hardening succeeded' } -Scope It
        }
    }

    Context "Multi-Computer Operations" {
        It "returns results for all computers" {
            Mock New-PSSession { return @(@{ ComputerName = 'Host1' }, @{ ComputerName = 'Host2' }, @{ ComputerName = 'Host3' }) }
            Mock Invoke-Command { return @{ AppliedRules = @(1, 2); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' } }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName @('Host1', 'Host2', 'Host3') -Profile Recommended

            $result | Should -HaveCount 3
            $result[0].ComputerName | Should -Be 'Host1'
            $result[1].ComputerName | Should -Be 'Host2'
            $result[2].ComputerName | Should -Be 'Host3'
        }

        It "reports success/failure per computer" {
            $invocationCount = 0
            $session1 = @{ ComputerName = 'Host1' }
            $session2 = @{ ComputerName = 'Host2' }
            Mock New-PSSession { return @($session1, $session2) }
            Mock Invoke-Command {
                $invocationCount++
                if ($invocationCount -eq 1) {
                    return @{ AppliedRules = @(1); FailedRules = @(); ComplianceReport = @{ CompliancePercentage = 100; Status = 'OK' }; Duration = '00:00:01' }
                }
                else {
                    throw [System.Exception]'Host2 error'
                }
            }
            Mock Remove-PSSession { }
            Mock Write-Log { }

            $result = Invoke-RemoteHardening -ComputerName @('Host1', 'Host2') -Profile Basis -ErrorAction SilentlyContinue

            $result[0].Success | Should -Be $true
            $result[1].Success | Should -Be $false
        }
    }
}