Tests/Policy.Tests.ps1

#Requires -Module Pester
<#
.SYNOPSIS
    Pester v5 tests for policy engine edge cases.
#>


BeforeAll {
    $modulePath = Join-Path (Split-Path $PSScriptRoot) 'LicenseGuard.psm1'
    Import-Module $modulePath -Force -ErrorAction Stop
    Initialize-LicenseGuard -Language en

    $script:policyContent = @'
{
  "rules": [
    { "id":"R001","category":"P2P","pattern":"BitTorrent","matchType":"contains","status":"PROHIBITED","reason":"P2P not permitted","severity":"HIGH","alternative":"None","referenceUrl":"" },
    { "id":"R002","category":"Remote","pattern":"^TeamViewer$","matchType":"regex","status":"REQUIRES_LICENSE","reason":"License required","severity":"MEDIUM","alternative":"","referenceUrl":"" },
    { "id":"R003","category":"Dev","pattern":"Python 3","matchType":"startsWith","status":"ALLOWED","reason":"Approved","severity":"LOW","alternative":"","referenceUrl":"" },
    { "id":"R004","category":"Exact","pattern":"ExactMatch","matchType":"exact","status":"PROHIBITED","reason":"Exact match test","severity":"HIGH","alternative":"","referenceUrl":"" }
  ]
}
'@

    $script:policyFile = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.json'
    $script:policyContent | Out-File $script:policyFile -Encoding UTF8
}

AfterAll {
    Remove-Item $script:policyFile -ErrorAction SilentlyContinue
    Remove-Module LicenseGuard -ErrorAction SilentlyContinue
}

Describe 'Invoke-LGPolicyCheck — match types' {
    It 'contains match: detects partial name' {
        $sw     = @([PSCustomObject]@{ Name='µTorrent BitTorrent Client'; Version='3.6'; Publisher='BitTorrent' })
        $result = Invoke-LGPolicyCheck -PolicyPath $script:policyFile -SoftwareCache $sw
        ($result | Where-Object { $_.RuleId -eq 'R001' }).PolicyStatus | Should -Be 'PROHIBITED'
    }

    It 'regex match: does not match partial name with ^ anchor' {
        $sw     = @([PSCustomObject]@{ Name='TeamViewer Host'; Version='15'; Publisher='TeamViewer GmbH' })
        $result = Invoke-LGPolicyCheck -PolicyPath $script:policyFile -SoftwareCache $sw
        # "TeamViewer Host" does NOT match ^TeamViewer$ — should be unmatched
        ($result | Where-Object { $_.RuleId -eq 'N/A' }) | Should -Not -BeNullOrEmpty
    }

    It 'regex match: matches exact name' {
        $sw     = @([PSCustomObject]@{ Name='TeamViewer'; Version='15'; Publisher='TeamViewer GmbH' })
        $result = Invoke-LGPolicyCheck -PolicyPath $script:policyFile -SoftwareCache $sw
        ($result | Where-Object { $_.RuleId -eq 'R002' }).PolicyStatus | Should -Be 'REQUIRES_LICENSE'
    }

    It 'startsWith match: matches prefix' {
        $sw     = @([PSCustomObject]@{ Name='Python 3.11.0'; Version='3.11.0'; Publisher='Python Software Foundation' })
        $result = Invoke-LGPolicyCheck -PolicyPath $script:policyFile -SoftwareCache $sw
        ($result | Where-Object { $_.RuleId -eq 'R003' }).PolicyStatus | Should -Be 'ALLOWED'
    }

    It 'exact match: does not match partial string' {
        $sw     = @([PSCustomObject]@{ Name='ExactMatch Suite'; Version='1'; Publisher='Co' })
        $result = Invoke-LGPolicyCheck -PolicyPath $script:policyFile -SoftwareCache $sw
        # "ExactMatch Suite" is NOT an exact match for "ExactMatch"
        ($result | Where-Object { $_.RuleId -eq 'N/A' }) | Should -Not -BeNullOrEmpty
    }

    It 'exact match: matches exactly' {
        $sw     = @([PSCustomObject]@{ Name='ExactMatch'; Version='1'; Publisher='Co' })
        $result = Invoke-LGPolicyCheck -PolicyPath $script:policyFile -SoftwareCache $sw
        ($result | Where-Object { $_.RuleId -eq 'R004' }).PolicyStatus | Should -Be 'PROHIBITED'
    }
}

Describe 'Invoke-LGPolicyCheck — severity mapping' {
    It 'maps PROHIBITED to HIGH severity when not explicit' {
        $minimalPolicy = '{"rules":[{"id":"X1","category":"Test","pattern":"Malware","matchType":"contains","status":"PROHIBITED","reason":"Bad","alternative":""}]}'
        $tmp = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.json'
        $minimalPolicy | Out-File $tmp -Encoding UTF8
        $sw     = @([PSCustomObject]@{ Name='Malware Tool'; Version='1'; Publisher='Bad' })
        $result = Invoke-LGPolicyCheck -PolicyPath $tmp -SoftwareCache $sw
        ($result | Where-Object { $_.RuleId -eq 'X1' }).Severity | Should -Be 'HIGH'
        Remove-Item $tmp
    }

    It 'maps REQUIRES_LICENSE to MEDIUM severity when not explicit' {
        $minimalPolicy = '{"rules":[{"id":"X2","category":"Test","pattern":"LicApp","matchType":"contains","status":"REQUIRES_LICENSE","reason":"Needs lic","alternative":""}]}'
        $tmp = [System.IO.Path]::GetTempFileName() -replace '\.tmp$', '.json'
        $minimalPolicy | Out-File $tmp -Encoding UTF8
        $sw     = @([PSCustomObject]@{ Name='LicApp 2.0'; Version='2'; Publisher='Lic' })
        $result = Invoke-LGPolicyCheck -PolicyPath $tmp -SoftwareCache $sw
        ($result | Where-Object { $_.RuleId -eq 'X2' }).Severity | Should -Be 'MEDIUM'
        Remove-Item $tmp
    }
}

Describe 'Get-LGRunningProcesses' {
    BeforeEach {
        Mock Get-Process {
            @(
                [PSCustomObject]@{ ProcessName = 'bittorrent' }
                [PSCustomObject]@{ ProcessName = 'chrome'     }
            )
        } -ModuleName LicenseGuard
    }

    It 'detects prohibited process running' {
        $findings = @([PSCustomObject]@{ Name='BitTorrent Client'; PolicyStatus='PROHIBITED' })
        $result   = Get-LGRunningProcesses -PolicyFindings $findings
        $result   | Should -Not -BeNullOrEmpty
        $result[0].Status | Should -Be 'EXPIRED'
    }

    It 'returns empty when prohibited process not running' {
        $findings = @([PSCustomObject]@{ Name='WireShark'; PolicyStatus='PROHIBITED' })
        $result   = Get-LGRunningProcesses -PolicyFindings $findings
        $result   | Should -BeNullOrEmpty
    }

    It 'returns empty when no prohibited software defined' {
        $findings = @([PSCustomObject]@{ Name='Chrome'; PolicyStatus='ALLOWED' })
        $result   = Get-LGRunningProcesses -PolicyFindings $findings
        $result   | Should -BeNullOrEmpty
    }
}