diff --git a/CHANGELOG.md b/CHANGELOG.md index 57c5cfc..79fcb73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Added public function `Find-Certificate` that returns one or more + certificates using certificate selector parameters - [Issue #100](https://github.com/dsccommunity/DscResource.Common/issues/100) + - Related to [CertificateDsc Issue #272](https://github.com/dsccommunity/CertificateDsc/issues/272). + ## [0.14.0] - 2022-12-31 ### Added diff --git a/README.md b/README.md index 4298fcf..6694682 100644 --- a/README.md +++ b/README.md @@ -521,6 +521,82 @@ ConvertTo-HashTable -CimInstance $cimInstance This creates a array om CimInstances of the class name MSFT_KeyValuePair and passes it to ConvertTo-HashTable which returns a hashtable. +### `Find-Certificate` + +A common function to find certificates based on multiple search filters, +including, but not limited to: Thumbprint, Friendly Name, DNS Names, +Key Usage, Issuers, etc. + +Locates one or more certificates using the passed certificate selector +parameters. + +If more than one certificate is found matching the selector criteria, +they will be returned in order of descending expiration date. + +#### Syntax + +```plaintext +Find-Certificate [[-Thumbprint] ] [[-FriendlyName] ] +[[-Subject] ] [[-DNSName] ] [[-Issuer] ] +[[-KeyUsage] ] [[-EnhancedKeyUsage] ] [[-Store] ] +[[-AllowExpired] ] [] +``` + +### Outputs + +**System.Security.Cryptography.X509Certificates.X509Certificate2** + +### Example + +```PowerShell +Find-Certificate -Thumbprint '1111111111111111111111111111111111111111' +``` + +Return certificate that matches thumbprint. + +```PowerShell +Find-Certificate -KeyUsage 'DataEncipherment', 'DigitalSignature' +``` + +Return certificate(s) that have specific key usage. + +```PowerShell +Find-Certificate -DNSName 'www.fabrikam.com', 'www.contoso.com' +``` + +Return certificate(s) filtered on specific DNS Names. + +```PowerShell +find-certificate -Subject 'CN=contoso, DC=com' +``` + +Return certificate(s) with specific subject. + +```PowerShell +find-certificate -Issuer 'CN=contoso-ca, DC=com' -AllowExpired $true +``` + +Return all certificates from specific issuer, including expired certificates. + +```PowerShell +$findCertSplat = @{ + EnhancedKeyUsage = @('Client authentication','Server Authentication') + AllowExpired = $true +} + +Find-Certificate @findCertSplat +``` + +Return all certificates that can be used for server or client authentication, +including expired certificates. + +```PowerShell +Find-Certificate -FriendlyName 'My SSL Cert' +``` + +Return certificate based on FriendlyName. + + ### `Get-ComputerName` Returns the computer name cross-plattform. The variable `$env:COMPUTERNAME` diff --git a/source/Public/Find-Certificate.ps1 b/source/Public/Find-Certificate.ps1 new file mode 100644 index 0000000..58523b4 --- /dev/null +++ b/source/Public/Find-Certificate.ps1 @@ -0,0 +1,189 @@ +<# + .SYNOPSIS + Locates one or more certificates using the passed certificate selector parameters. + + If more than one certificate is found matching the selector criteria, they will be + returned in order of descending expiration date. + + .DESCRIPTION + A common function to find certificates based on multiple search filters, including, + but not limited to: Thumbprint, Friendly Name, DNS Names, Key Usage, Issuers, etc. + + .PARAMETER Thumbprint + The thumbprint of the certificate to find. + + .PARAMETER FriendlyName + The friendly name of the certificate to find. + + .PARAMETER Subject + The subject of the certificate to find. + + .PARAMETER DNSName + The subject alternative name of the certificate to export must contain these values. + + .PARAMETER Issuer + The issuer of the certificate to find. + + .PARAMETER KeyUsage + The key usage of the certificate to find must contain these values. + + .PARAMETER EnhancedKeyUsage + The enhanced key usage of the certificate to find must contain these values. + + .PARAMETER Store + The Windows Certificate Store Name to search for the certificate in. + Defaults to 'My'. + + .PARAMETER AllowExpired + Allows expired certificates to be returned. + + .EXAMPLE + Find-Certificate -Thumbprint '1111111111111111111111111111111111111111' + + Return certificate that matches thumbprint. + + .EXAMPLE + Find-Certificate -KeyUsage 'DataEncipherment', 'DigitalSignature' + + Return certificate(s) that have specific key usage. + + .EXAMPLE + Find-Certificate -DNSName 'www.fabrikam.com', 'www.contoso.com' + + Return certificate(s) filtered on specific DNS Names. + + .EXAMPLE + find-certificate -Subject 'CN=contoso, DC=com' + + Return certificate(s) with specific subject. + + .EXAMPLE + find-certificate -Issuer 'CN=contoso-ca, DC=com' -AllowExpired $true + + Return all certificates from specific issuer, including expired certificates. + + .EXAMPLE + Find-Certificate -EnhancedKeyUsage 'Server Authentication' -AllowExpired $true + + Return all certificates that can be used for "Server Authentication", including expired certificates. + + .EXAMPLE + Find-Certificate -FriendlyName 'My IIS Site SSL Cert' + + Return certificate based on FriendlyName. + +#> +function Find-Certificate +{ + [CmdletBinding()] + [OutputType([System.Security.Cryptography.X509Certificates.X509Certificate2[]])] + param + ( + [Parameter()] + [System.String] + $Thumbprint, + + [Parameter()] + [System.String] + $FriendlyName, + + [Parameter()] + [System.String] + $Subject, + + [Parameter()] + [System.String[]] + $DNSName, + + [Parameter()] + [System.String] + $Issuer, + + [Parameter()] + [System.String[]] + $KeyUsage, + + [Parameter()] + [System.String[]] + $EnhancedKeyUsage, + + [Parameter()] + [System.String] + $Store = 'My', + + [Parameter()] + [Boolean] + $AllowExpired = $false + ) + + $certPath = Join-Path -Path 'Cert:\LocalMachine' -ChildPath $Store + + if (-not (Test-Path -Path $certPath)) + { + # The Certificte Path is not valid + New-InvalidArgumentException ` + -Message ($script:localizedData.CertificatePathError -f $certPath) ` + -ArgumentName 'Store' + } # if + + # Assemble the filter to use to select the certificate + $certFilters = @() + + if ($PSBoundParameters.ContainsKey('Thumbprint')) + { + $certFilters += @('($_.Thumbprint -eq $Thumbprint)') + } # if + + if ($PSBoundParameters.ContainsKey('FriendlyName')) + { + $certFilters += @('($_.FriendlyName -eq $FriendlyName)') + } # if + + if ($PSBoundParameters.ContainsKey('Subject')) + { + $certFilters += @('($_.Subject -eq $Subject)') + } # if + + if ($PSBoundParameters.ContainsKey('Issuer')) + { + $certFilters += @('($_.Issuer -eq $Issuer)') + } # if + + if (-not $AllowExpired) + { + $certFilters += @('(((Get-Date) -le $_.NotAfter) -and ((Get-Date) -ge $_.NotBefore))') + } # if + + if ($PSBoundParameters.ContainsKey('DNSName')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject $_.DNSNameList.Unicode -DifferenceObject $DNSName | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + if ($PSBoundParameters.ContainsKey('KeyUsage')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject ($_.Extensions.KeyUsages -split ", ") -DifferenceObject $KeyUsage | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + if ($PSBoundParameters.ContainsKey('EnhancedKeyUsage')) + { + $certFilters += @('(@(Compare-Object -ReferenceObject ($_.EnhancedKeyUsageList.FriendlyName) -DifferenceObject $EnhancedKeyUsage | Where-Object -Property SideIndicator -eq "=>").Count -eq 0)') + } # if + + # Join all the filters together + $certFilterScript = '(' + ($certFilters -join ' -and ') + ')' + + Write-Verbose ` + -Message ($script:localizedData.SearchingForCertificateUsingFilters -f $store, $certFilterScript) ` + -Verbose + + $certs = Get-ChildItem -Path $certPath | + Where-Object -FilterScript ([ScriptBlock]::Create($certFilterScript)) + + # Sort the certificates + if ($certs.count -gt 1) + { + $certs = $certs | Sort-Object -Descending -Property 'NotAfter' + } # if + + return $certs +} # end function Find-Certificate diff --git a/source/en-US/DscResource.Common.strings.psd1 b/source/en-US/DscResource.Common.strings.psd1 index 8acc2d1..c49bff2 100644 --- a/source/en-US/DscResource.Common.strings.psd1 +++ b/source/en-US/DscResource.Common.strings.psd1 @@ -43,4 +43,8 @@ ConvertFrom-StringData @' ## Assert-RequiredCommandParameter RequiredCommandParameter_SpecificParametersMustAllBeSet = The parameters '{0}' must all be specified. (DRC0044) RequiredCommandParameter_SpecificParametersMustAllBeSetWhenParameterExist = The parameters '{0}' must all be specified if either parameter '{1}' is specified. (DRC0045) + + ## Find-Certificate + CertificatePathError = Certificate Path '{0}' is not valid. (DRC0046) + SearchingForCertificateUsingFilters = Looking for certificate in Store '{0}' using filter '{1}'. (DRC0047) '@ diff --git a/tests/Unit/Public/Find-Certificate.Tests.ps1 b/tests/Unit/Public/Find-Certificate.Tests.ps1 new file mode 100644 index 0000000..796a412 --- /dev/null +++ b/tests/Unit/Public/Find-Certificate.Tests.ps1 @@ -0,0 +1,522 @@ +[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] +param () + +BeforeDiscovery { + try + { + if (-not (Get-Module -Name 'DscResource.Test')) + { + # Assumes dependencies has been resolved, so if this module is not available, run 'noop' task. + if (-not (Get-Module -Name 'DscResource.Test' -ListAvailable)) + { + # Redirect all streams to $null, except the error stream (stream 2) + & "$PSScriptRoot/../../build.ps1" -Tasks 'noop' 2>&1 4>&1 5>&1 6>&1 > $null + } + + # If the dependencies has not been resolved, this will throw an error. + Import-Module -Name 'DscResource.Test' -Force -ErrorAction 'Stop' + } + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -ResolveDependency -Tasks build" first.' + } +} + +BeforeAll { + $script:moduleName = 'DscResource.Common' + + # Make sure there are not other modules imported that will conflict with mocks. + Get-Module -Name $script:moduleName -All | Remove-Module -Force + + # Re-import the module using force to get any code changes between runs. + Import-Module -Name $script:moduleName -Force -ErrorAction 'Stop' + + $PSDefaultParameterValues['InModuleScope:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Mock:ModuleName'] = $script:moduleName + $PSDefaultParameterValues['Should:ModuleName'] = $script:moduleName + + # Dynamic mock content for Get-ChildItem + $mockGetChildItem = { + switch ( $Path ) + { + 'cert:\LocalMachine\My' + { + return @( $validCertificate ) + } + + 'cert:\LocalMachine\NoCert' + { + return @() + } + + 'cert:\LocalMachine\TwoCerts' + { + return @( $expiredCertificate, $validCertificate ) + } + + 'cert:\LocalMachine\Expired' + { + return @( $expiredCertificate ) + } + + default + { + throw 'mock called with unexpected value {0}' -f $Path + } + } + } + + $mockJoinPath = { + return "Cert:\LocalMachine\$ChildPath" + } + + $mockTestPath = { + return $true + } +} + +AfterAll { + $PSDefaultParameterValues.Remove('Mock:ModuleName') + $PSDefaultParameterValues.Remove('InModuleScope:ModuleName') + $PSDefaultParameterValues.Remove('Should:ModuleName') + + Remove-Module -Name $script:moduleName +} + +Describe 'Find-Certificate' -Tag 'FindCertificate' { + BeforeAll { + Mock -CommandName Get-ChildItem -MockWith $mockGetChildItem + Mock -CommandName Test-Path -MockWith $mockTestPath + Mock -CommandName Join-Path -MockWith $mockJoinPath + + #Generate test cert object to run against. + $certificateDNSNames = @('www.fabrikam.com', 'www.contoso.com') + $certificateDNSNamesReverse = @('www.contoso.com', 'www.fabrikam.com') + $certificateDNSNamesNoMatch = $certificateDNSNames + @('www.nothere.com') + + $certificateKeyUsage = @('DigitalSignature', 'DataEncipherment') + $certificateKeyUsageReverse = @('DataEncipherment', 'DigitalSignature') + $certificateKeyUsageNoMatch = $certificateKeyUsage + @('KeyEncipherment') + + $certificateEKU = @('Server Authentication', 'Client authentication') + $certificateEKUReverse = @('Client authentication', 'Server Authentication') + $certificateEKUNoMatch = $certificateEKU + @('Encrypting File System') + + $certificateSubject = 'CN=contoso, DC=com' + $certificateFriendlyName = 'Contoso Test Cert' + + $validThumbprint = 'B994DA47197931EFA3B00CB2DF34E2510E404C8D' + $expiredThumbprint = '31343B742B3062CF880487C2125E851E2884D00A' + $noCertificateThumbprint = '1111111111111111111111111111111111111111' + + $validCertificate = @{ + FriendlyName = $certificateFriendlyName + Subject = $certificateSubject + Thumbprint = $validThumbprint + NotBefore = ((Get-Date) - (New-TimeSpan -Days 1)) + NotAfter = ((Get-Date) + (New-TimeSpan -Days 30)) + Issuer = $certificateSubject + + DnsNameList = $certificateDNSNames | ForEach-Object { + @{ + Unicode = $PSItem + } + } + + Extensions = @{ + KeyUsages = $certificateKeyUsage -join ", " + } + + EnhancedKeyUsageList = $certificateEKU | ForEach-Object { + @{ + FriendlyName = $PSItem + } + } + } + + $expiredCertificate = $validCertificate.Clone() + $expiredCertificate['Thumbprint'] = $expiredThumbprint + $expiredCertificate['NotBefore'] = ((Get-Date) - (New-TimeSpan -Days 2)) + $expiredCertificate['NotAfter'] = ((Get-Date) - (New-TimeSpan -Days 1)) + } + + Context 'Thumbprint only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $validThumbprint } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'Thumbprint only is passed and matching certificate does not exist' { + It 'Should not throw exception' { + + { $script:result = Find-Certificate -Thumbprint $noCertificateThumbprint } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'FriendlyName only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certificateFriendlyName } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'FriendlyName only is passed and matching certificate does not exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -FriendlyName 'Does Not Exist' } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'Subject only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -Subject $certificateSubject } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'Subject only is passed and matching certificate does not exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -Subject 'CN=Does Not Exist' } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'Issuer only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -Issuer $certificateSubject } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'Issuer only is passed and matching certificate does not exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -Issuer 'CN=Does Not Exist' } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'DNSName only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -DnsName $certificateDNSNames } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'DNSName only is passed in reversed order and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -DnsName $certificateDNSNamesReverse } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'DNSName only is passed with only one matching DNS name and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -DnsName $certificateDNSNames[0] } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'DNSName only is passed but an entry is missing and matching certificate does not exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -DnsName $certificateDNSNamesNoMatch } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'KeyUsage only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certificateKeyUsage } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'KeyUsage only is passed in reversed order and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certificateKeyUsageReverse } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'KeyUsage only is passed with only one matching DNS name and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certificateKeyUsage[0] } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'KeyUsage only is passed but an entry is missing and matching certificate does not exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -KeyUsage $certificateKeyUsageNoMatch } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'EnhancedKeyUsage only is passed and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certificateEKU } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'EnhancedKeyUsage only is passed in reversed order and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certificateEKUReverse } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'EnhancedKeyUsage only is passed with only one matching DNS name and matching certificate exists' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certificateEKU[0] } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'EnhancedKeyUsage only is passed but an entry is missing and matching certificate does not exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -EnhancedKeyUsage $certificateEKUNoMatch } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'Store only is passed but the path to the certificate store does not exist' { + BeforeAll { + Mock -CommandName Test-Path -MockWith { return $false } + } + It 'Should throw exception' { + { $script:result = Find-Certificate -Store "MockWillForceFailureOfTestPath" } | Should -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 0 -Scope "context" + } + } + + Context 'Thumbprint only is passed and matching certificate does not exist in the store' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -Thumbprint $validThumbprint -Store 'NoCert' } | Should -Not -Throw + } + + It 'Should return null' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'FriendlyName only is passed and both valid and expired certificates exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certificateFriendlyName -Store 'TwoCerts' } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $validThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'FriendlyName only is passed and only expired certificates exist' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certificateFriendlyName -Store 'Expired' } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result | Should -BeNullOrEmpty + } + + It 'Should call expected mocks' { + Should -Invoke -CommandName Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke -CommandName Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } + + Context 'FriendlyName only is passed and only expired certificates exist but allowexpired passed' { + It 'Should not throw exception' { + { $script:result = Find-Certificate -FriendlyName $certificateFriendlyName -Store 'Expired' -AllowExpired:$true } | Should -Not -Throw + } + + It 'Should return expected certificate' { + $script:result.Thumbprint | Should -Be $expiredThumbprint + } + + It 'Should call expected mocks' { + Should -Invoke Test-Path -Exactly -Times 1 -Scope "context" + Should -Invoke Get-ChildItem -Exactly -Times 1 -Scope "context" + } + } +}