Tests/Public/Get-PWSHCertutilIssuedCerts.Tests.ps1

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

    $testJson = @'
{
  "version": "1.0",
  "profiles": {
    "test-profile": {
      "description": "Test",
      "defaultProfile": true,
      "remoting": { "useTls": true, "port": 5986, "maxSessionsPerCA": 2 },
      "cas": [
        { "fqdn": "ca01.test.local", "displayName": "CA01" },
        { "fqdn": "ca02.test.local", "displayName": "CA02" }
      ],
      "certutilView": {
        "restrict": { "issuedCerts": "GeneralFlags=0,Disposition=20" },
        "out": { "issuedCerts": ["RequestID","CommonName"] }
      },
      "syncState": {
        "lastSync": "2026-01-01T00:00:00Z",
        "fieldNameMap": { "RequestID": "RequestID", "CommonName": "CommonName" }
      }
    }
  }
}
'@

    $script:TestConfigPath = [IO.Path]::GetTempFileName()
    Set-Content -Path $script:TestConfigPath -Value $testJson -Encoding UTF8
    InModuleScope Posh-Certutil -Parameters @{ ConfigPath = $script:TestConfigPath } {
        param($ConfigPath)
        $script:ConfigPath = $ConfigPath
    }

    $fakeCsvOutput = @('"RequestID","CommonName"','"1","server01.corp.local"')
    $fakeSession   = New-MockObject -Type System.Management.Automation.Runspaces.PSSession
}

AfterAll {
    Remove-Item -Path $script:TestConfigPath -ErrorAction SilentlyContinue
    Remove-Module Posh-Certutil -ErrorAction SilentlyContinue
}

Describe 'Get-PWSHCertutilIssuedCerts' -Tag Unit {
    BeforeEach {
        Mock -ModuleName Posh-Certutil Get-CASession     { $fakeSession }
        Mock -ModuleName Posh-Certutil Get-CACulture     { 'en-US' }
        Mock -ModuleName Posh-Certutil Invoke-CertutilView { $fakeCsvOutput }
    }

    It 'Queries all CAs in the profile when -CAFqdn is not specified' {
        Get-PWSHCertutilIssuedCerts -Profile 'test-profile' | Out-Null
        Should -Invoke -ModuleName Posh-Certutil Get-CASession -Times 2
    }

    It 'Queries only the specified CA when -CAFqdn is provided' {
        Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -CAFqdn 'ca01.test.local' | Out-Null
        Should -Invoke -ModuleName Posh-Certutil Get-CASession -Times 1
    }

    It 'Throws when the specified -CAFqdn is not in the profile' {
        { Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -CAFqdn 'ca99.test.local' } |
            Should -Throw -ExpectedMessage "*ca99.test.local*"
    }

    It 'Stamps Profile and CAServer on each returned object' {
        $result = Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -CAFqdn 'ca01.test.local'
        $result.Profile  | Should -Be 'test-profile'
        $result.CAServer | Should -Be 'ca01.test.local'
    }

    It 'Returns empty collection when certutil returns no data rows' {
        Mock -ModuleName Posh-Certutil Invoke-CertutilView { @('"RequestID","CommonName"') }
        $result = Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -CAFqdn 'ca01.test.local'
        $result | Should -BeNullOrEmpty
    }

    It 'Writes an error but continues when one CA fails' {
        Mock -ModuleName Posh-Certutil Get-CASession {
            if ($CAFqdn -eq 'ca01.test.local') { throw 'connection refused' }
            $fakeSession
        }
        { Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -ErrorAction SilentlyContinue } |
            Should -Not -Throw
    }

    It 'Returns objects with canonical property names when FieldMap is applied' {
        $localizedCsv = @('"Issued Request ID","Issued Common Name"', '"1","server01.corp.local"')
        Mock -ModuleName Posh-Certutil Invoke-CertutilView { $localizedCsv }
        InModuleScope Posh-Certutil -Parameters @{ ConfigPath = $script:TestConfigPath } {
            param($ConfigPath)
            $localizedMap = @{ 'Issued Request ID' = 'RequestID'; 'Issued Common Name' = 'CommonName' }
            $json = Get-Content -Path $ConfigPath -Raw | ConvertFrom-Json
            $json.profiles.'test-profile'.syncState.fieldNameMap |
                Add-Member -MemberType NoteProperty -Name 'Issued Request ID'  -Value 'RequestID'  -Force
            $json.profiles.'test-profile'.syncState.fieldNameMap |
                Add-Member -MemberType NoteProperty -Name 'Issued Common Name' -Value 'CommonName' -Force
            $json | ConvertTo-Json -Depth 10 | Set-Content -Path $ConfigPath -Encoding UTF8
            $script:ConfigPath = $ConfigPath
        }
        $result = Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -CAFqdn 'ca01.test.local'
        $result.RequestID  | Should -Be '1'
        $result.CommonName | Should -Be 'server01.corp.local'
    }

    It 'Falls back to the default profile when -Profile is omitted' {
        $result = Get-PWSHCertutilIssuedCerts -CAFqdn 'ca01.test.local'
        $result.Profile | Should -Be 'test-profile'
    }

    It 'Throws when -Profile is omitted and no default profile is configured' {
        $noDefaultPath = [IO.Path]::GetTempFileName()
        '{"version":"1.0","profiles":{"other":{"description":"x","defaultProfile":false,"remoting":{"useTls":true,"port":5986,"maxSessionsPerCA":2},"cas":[],"certutilView":{"restrict":{},"out":{}}}}}' |
            Set-Content -Path $noDefaultPath -Encoding UTF8
        InModuleScope Posh-Certutil -Parameters @{ ConfigPath = $noDefaultPath } {
            param($ConfigPath)
            $script:ConfigPath = $ConfigPath
        }
        try {
            { Get-PWSHCertutilIssuedCerts } | Should -Throw -ExpectedMessage '*default profile*'
        } finally {
            Remove-Item -Path $noDefaultPath -ErrorAction SilentlyContinue
            InModuleScope Posh-Certutil -Parameters @{ ConfigPath = $script:TestConfigPath } {
                param($ConfigPath)
                $script:ConfigPath = $ConfigPath
            }
        }
    }

    It '-Profile is a dynamic parameter that offers tab completion for configured profiles' {
        $line    = 'Get-PWSHCertutilIssuedCerts -Profile '
        $results = TabExpansion2 -inputScript $line -cursorColumn $line.Length
        $results.CompletionMatches.CompletionText | Should -Contain 'test-profile'
    }

    It 'Rejects an unknown -Profile value before it ever reaches Resolve-ProfileName' {
        { Get-PWSHCertutilIssuedCerts -Profile 'does-not-exist' -ErrorAction Stop } |
            Should -Throw -ExpectedMessage '*ValidateSet*'
    }

    It 'Returns NotBefore/NotAfter as DateTime, not strings, using the CA culture' {
        Mock -ModuleName Posh-Certutil Invoke-CertutilView {
            @('"RequestID","CommonName","NotBefore","NotAfter"',
              '"1","server01.corp.local","1/1/2025 12:00 AM","1/1/2026 12:00 AM"')
        }
        $result = Get-PWSHCertutilIssuedCerts -Profile 'test-profile' -CAFqdn 'ca01.test.local'
        $result.NotBefore | Should -BeOfType [datetime]
        $result.NotAfter  | Should -BeOfType [datetime]
        Should -Invoke -ModuleName Posh-Certutil Get-CACulture -Times 1
    }
}