Tests/Private/Config.Tests.ps1

BeforeDiscovery {
    Import-Module (Resolve-Path "$PSScriptRoot\..\..\Posh-Certutil.psd1") -Force
}

BeforeAll {
    Import-Module (Resolve-Path "$PSScriptRoot\..\..\Posh-Certutil.psd1") -Force
}

AfterAll {
    Remove-Module Posh-Certutil -ErrorAction SilentlyContinue
}

InModuleScope Posh-Certutil {

    Describe 'Read-ConfigFile' -Tag Unit {
        BeforeEach {
            $testJson = @'
{
  "version": "1.0",
  "profiles": {
    "test-profile": {
      "description": "Test",
      "remoting": { "useTls": true, "port": 5986, "maxSessionsPerCA": 2 },
      "cas": [{ "fqdn": "ca01.test.local", "displayName": "CA01" }],
      "certutilView": {
        "restrict": { "issuedCerts": "GeneralFlags=0,Disposition=20" },
        "out": { "issuedCerts": ["RequestID","CommonName"] }
      }
    }
  }
}
'@

            $tempPath = [IO.Path]::GetTempFileName()
            Set-Content -Path $tempPath -Value $testJson -Encoding UTF8
            $script:ConfigPath = $tempPath
        }

        AfterEach {
            Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
            $script:ConfigPath = Join-Path $script:ModuleRoot 'Config\Posh-Certutil.json'
        }

        Context 'When the config file exists' {
            It 'Returns a PSCustomObject with profiles' {
                $result = Read-ConfigFile
                $result | Should -Not -BeNullOrEmpty
                $result.profiles.PSObject.Properties.Name | Should -Contain 'test-profile'
            }
        }

        Context 'When the config file does not exist' {
            It 'Returns an empty profiles object without throwing' {
                $script:ConfigPath = 'C:\DoesNotExist\missing.json'
                $result = Read-ConfigFile
                $result.profiles.PSObject.Properties.Name | Should -BeNullOrEmpty
            }
        }
    }

    Describe 'Get-ProfileConfig' -Tag Unit {
        BeforeAll {
            $mockConfig = [PSCustomObject]@{
                profiles = [PSCustomObject]@{
                    'prod-pki' = [PSCustomObject]@{ description = 'Production' }
                }
            }
        }

        Context 'When the profile exists' {
            It 'Returns the profile object' {
                $result = Get-ProfileConfig -Config $mockConfig -ProfileName 'prod-pki'
                $result.description | Should -Be 'Production'
            }
        }

        Context 'When the profile does not exist' {
            It 'Throws with the available profile names in the message' {
                { Get-ProfileConfig -Config $mockConfig -ProfileName 'nonexistent' } |
                    Should -Throw -ExpectedMessage '*prod-pki*'
            }
        }
    }

    Describe 'Invoke-ProfileAutoSync' -Tag Unit {
        BeforeAll {
            $script:SyncTestJson = @'
{
  "version": "1.0",
  "profiles": {
    "no-sync": {
      "description": "Not yet synced",
      "remoting": { "useTls": true, "port": 5986, "maxSessionsPerCA": 2 },
      "cas": [{ "fqdn": "ca01.test.local", "displayName": "CA01" }],
      "certutilView": {
        "restrict": { "issuedCerts": "Disposition=20" },
        "out": { "issuedCerts": ["RequestID","CommonName"] }
      },
      "syncState": null
    },
    "already-synced": {
      "description": "Already synced",
      "remoting": { "useTls": true, "port": 5986, "maxSessionsPerCA": 2 },
      "cas": [{ "fqdn": "ca01.test.local", "displayName": "CA01" }],
      "certutilView": {
        "restrict": { "issuedCerts": "Disposition=20" },
        "out": { "issuedCerts": ["RequestID","CommonName"] }
      },
      "syncState": {
        "lastSync": "2026-01-01T00:00:00Z",
        "fieldNameMap": { "RequestID": "RequestID" }
      }
    }
  }
}
'@

        }

        BeforeEach {
            $syncTempPath = [IO.Path]::GetTempFileName()
            Set-Content -Path $syncTempPath -Value $script:SyncTestJson -Encoding UTF8
            $script:ConfigPath = $syncTempPath
        }

        AfterEach {
            Remove-Item -Path $syncTempPath -ErrorAction SilentlyContinue
            $script:ConfigPath = Join-Path $script:ModuleRoot 'Config\Posh-Certutil.json'
        }

        It 'Returns profileConfig unchanged when syncState.lastSync is already set' {
            $cfg     = Read-ConfigFile
            $profile = Get-ProfileConfig -Config $cfg -ProfileName 'already-synced'
            $result  = Invoke-ProfileAutoSync -Config $cfg -ProfileName 'already-synced' -ProfileConfig $profile
            $result.syncState.lastSync | Should -Not -BeNullOrEmpty
            $result.syncState.fieldNameMap.RequestID | Should -Be 'RequestID'
        }

        It 'Emits a warning and calls Get-CertutilFieldNameMap when no syncState exists' {
            $fakeSession = New-MockObject -Type System.Management.Automation.Runspaces.PSSession
            Mock Get-CASession { $fakeSession }
            Mock Get-CertutilFieldNameMap { @{ 'Issued Request ID' = 'RequestID' } }

            $cfg     = Read-ConfigFile
            $profile = Get-ProfileConfig -Config $cfg -ProfileName 'no-sync'

            $warnings = $null
            Invoke-ProfileAutoSync -Config $cfg -ProfileName 'no-sync' -ProfileConfig $profile `
                -WarningVariable warnings -WarningAction SilentlyContinue | Out-Null

            $warnings | Should -Not -BeNullOrEmpty
            $warnings[0] | Should -Match 'no-sync'
            Should -Invoke Get-CertutilFieldNameMap -Times 1
        }

        It 'Emits a warning and returns profileConfig unchanged when auto-sync fails' {
            Mock Get-CASession { throw 'WinRM unreachable' }

            $cfg     = Read-ConfigFile
            $profile = Get-ProfileConfig -Config $cfg -ProfileName 'no-sync'

            $warnings = $null
            $result = Invoke-ProfileAutoSync -Config $cfg -ProfileName 'no-sync' -ProfileConfig $profile `
                          -WarningVariable warnings -WarningAction SilentlyContinue
            $warnings | Where-Object { $_ -match 'Auto-sync failed' } | Should -Not -BeNullOrEmpty
            $result.syncState | Should -BeNullOrEmpty
        }
    }

    Describe 'Get-CertutilViewParams' -Tag Unit {
        BeforeAll {
            $mockProfile = [PSCustomObject]@{
                certutilView = [PSCustomObject]@{
                    restrict = [PSCustomObject]@{
                        issuedCerts   = 'GeneralFlags=0,Disposition=20'
                        expiringCerts = 'GeneralFlags=0,NotAfter>={EXPIRE_DATE}'
                    }
                    out = [PSCustomObject]@{
                        issuedCerts   = @('RequestID', 'CommonName')
                        expiringCerts = @('RequestID', 'NotAfter')
                    }
                }
            }
        }

        It 'Returns Restrict and Out strings for issuedCerts' {
            $result = Get-CertutilViewParams -ProfileConfig $mockProfile -Operation 'issuedCerts'
            $result.Restrict | Should -Be 'GeneralFlags=0,Disposition=20'
            $result.Out      | Should -Be 'RequestID,CommonName'
        }

        It 'Substitutes {EXPIRE_DATE} token' {
            $date   = '12/31/2025'
            $result = Get-CertutilViewParams -ProfileConfig $mockProfile -Operation 'expiringCerts' `
                          -Substitutions @{ EXPIRE_DATE = $date }
            $result.Restrict | Should -BeLike "*$date*"
            $result.Restrict | Should -Not -BeLike '*{EXPIRE_DATE}*'
        }

        It 'Throws when the operation key is missing from restrict' {
            { Get-CertutilViewParams -ProfileConfig $mockProfile -Operation 'revokedCerts' } |
                Should -Throw
        }
    }
}