Six Mock Get-ChildItem statements in tests/Get-PSBuildCertificate.tests.ps1 are declared without -ModuleName 'PowerShellBuild', so they never reach the call inside the module. Three of the tests that rely on them pass only because the machine running them has no code-signing certificate installed.
What happens
Pester mocks are scoped to the session state they are declared in. A mock declared in a test file replaces the command for the test file; it does not replace the command for a function running inside an imported module. Get-PSBuildCertificate calls Get-ChildItem -Path $CertStoreLocation -CodeSigningCert from inside PowerShellBuild, so it gets the real cmdlet and the real certificate store.
The six unscoped mocks:
| Line |
Test |
| 24 |
Defaults to Auto mode when no CertificateSource is specified |
| 42 |
Resolves to Store mode when SIGNCERTIFICATE environment variable is not set |
| 61 |
Returns $null when no valid certificate is found |
| 67 |
Filters out expired certificates |
| 76 |
Filters out certificates without a private key |
| 100 |
Returns $null when the specified thumbprint is not found |
Measured
Adding Should -Invoke Get-ChildItem -Times 0 -Exactly to a copy of Returns $null when no valid certificate is found — an assertion that the mock was never called — passes. A module-scoped spy added alongside it records exactly one call, which is the one the function actually makes:
[+] A: the unscoped mock is never invoked
[+] B: the real Get-ChildItem inside the module is what actually runs
Should -Invoke Get-ChildItem -Times 0 -Exactly (unscoped)
Should -Invoke -ModuleName PowerShellBuild Get-ChildItem -Times 1 -Exactly
Standing in a valid code-signing certificate for what the store would return, and keeping the test's own assertion verbatim:
[-] C: the same test fails when the store holds a code-signing certificate
Expected $null or empty, but got @{Subject=CN=Real Workstation Cert; ...}.
at $cert | Should -BeNullOrEmpty
Three tests fail that way on a machine with a code-signing certificate in Cert:\CurrentUser\My: Returns $null when no valid certificate is found, Filters out expired certificates, and Filters out certificates without a private key. All three assert $cert | Should -BeNullOrEmpty after asking the real store for a real certificate.
The other three unscoped mocks are harmless by luck rather than design. The two Auto-mode tests assert on the first verbose record, which is written before the store is touched, and the thumbprint test asks for 'NOTFOUND123', which no real certificate matches.
The same fix closes a coverage gap
Get-PSBuildCertificate's post-load validation block has never executed. It is the block that gates a certificate loaded from EnvVar or PfxFile on having a private key, not being expired, and carrying the Code Signing extended key usage:
if ($cert -and -not $SkipValidation -and ($resolvedSource -eq 'EnvVar' -or $resolvedSource -eq 'PfxFile')) {
if (-not $cert.HasPrivateKey) { throw ... }
if ($cert.NotAfter -le (Get-Date)) { throw ... }
$hasCodeSigningEku = $cert.EnhancedKeyUsageList | Where-Object { $_.ObjectId -eq $codeSigningOid }
if (-not $hasCodeSigningEku) { throw ... }
Every EnvVar and PfxFile test in the file feeds the function deliberately invalid data — [byte[]]@(1,2,3,4,5) base64-encoded, or an empty file with a .pfx extension — so construction of the X509Certificate2 throws before the block is reached. Nothing produces a certificate object that survives to line 228.
Code coverage over Get-PSBuildCertificate.ps1 with the whole file running:
26 tests passed, 0 failed
65 of 95 commands covered
Missed, lines 230-246: the entire private key, expiry, and Code Signing EKU block
This is the validation that decides whether a certificate a consumer supplied through a CI secret is usable. It has never run.
Why it matters
- The three store tests are latent CI-versus-workstation failures. They are green on the hosted runners and on any workstation without a signing certificate, and red on exactly the machine most likely to have one: a maintainer's, where signing is actually being worked on. That is the worst place for a test to first go red, because the natural reading is "my machine is broken", not "the test was never testing anything".
- The three store tests assert nothing about the module either way. With the mock inert,
Filters out expired certificates and Filters out certificates without a private key are the same test as Returns $null when no valid certificate is found: three identical assertions that the real store happens to be empty. Neither filter is exercised.
- The EKU and expiry checks are the signing feature's safety net, and the failure mode if they regress is a build that signs with a certificate it should have refused, or throws on one it should have accepted.
The model to follow is already in the file
The SkipValidation for store-backed sources context added in #194 is written correctly:
Mock -ModuleName PowerShellBuild -CommandName Get-ChildItem -MockWith { $script:expiredCertificate }
Nine tests there control what the store returns, and they are the only tests in the file that actually exercise the selection logic. Whatever is done here should look like those.
Options
- Add
-ModuleName 'PowerShellBuild' to the six mocks and give the three store tests real fixtures. Cheapest change, and it makes Filters out expired certificates and Filters out certificates without a private key do what their names say by returning an expired and a keyless certificate respectively. It also makes them deterministic everywhere.
- Cover the post-load validation block by generating a real certificate.
New-SelfSignedCertificate -Type CodeSigningCert produces one with a private key and the Code Signing EKU, and it can be exported to a PFX under $TestDrive to drive the PfxFile source, or base64-encoded into an environment variable to drive EnvVar. Windows-only, and it writes to the certificate store, so it needs a cleanup block. A certificate with -NotAfter in the past covers the expiry throw, and one created without the code-signing type covers the EKU throw.
- Cover the validation block with a fake object instead. The block only reads
HasPrivateKey, NotAfter, Subject, and EnhancedKeyUsageList, so a PSCustomObject would exercise every branch — but only if the loading step can be intercepted, and it constructs X509Certificate2 directly rather than through a mockable command. That would mean extracting the load behind a private function first.
- Delete the three store tests that assert the machine has no certificate. Honest about what they currently test, and it removes the latent failure without pretending to add coverage. Worth considering only if (1) is not taken.
(1) and (2) together are the whole fix, and (1) alone is worth doing even if (2) is deferred, because it removes the machine dependency.
Related: #194 (the correctly scoped context), #197 (the platform guard on these same tests). Shares its mock-scoping defect and its remediation with #217, which has the same unscoped Mock problem in tests/Invoke-PSBuildModuleSigning.tests.ps1.
Six
Mock Get-ChildItemstatements intests/Get-PSBuildCertificate.tests.ps1are declared without-ModuleName 'PowerShellBuild', so they never reach the call inside the module. Three of the tests that rely on them pass only because the machine running them has no code-signing certificate installed.What happens
Pester mocks are scoped to the session state they are declared in. A mock declared in a test file replaces the command for the test file; it does not replace the command for a function running inside an imported module.
Get-PSBuildCertificatecallsGet-ChildItem -Path $CertStoreLocation -CodeSigningCertfrom insidePowerShellBuild, so it gets the real cmdlet and the real certificate store.The six unscoped mocks:
Defaults to Auto mode when no CertificateSource is specifiedResolves to Store mode when SIGNCERTIFICATE environment variable is not setReturns $null when no valid certificate is foundFilters out expired certificatesFilters out certificates without a private keyReturns $null when the specified thumbprint is not foundMeasured
Adding
Should -Invoke Get-ChildItem -Times 0 -Exactlyto a copy ofReturns $null when no valid certificate is found— an assertion that the mock was never called — passes. A module-scoped spy added alongside it records exactly one call, which is the one the function actually makes:Standing in a valid code-signing certificate for what the store would return, and keeping the test's own assertion verbatim:
Three tests fail that way on a machine with a code-signing certificate in
Cert:\CurrentUser\My:Returns $null when no valid certificate is found,Filters out expired certificates, andFilters out certificates without a private key. All three assert$cert | Should -BeNullOrEmptyafter asking the real store for a real certificate.The other three unscoped mocks are harmless by luck rather than design. The two Auto-mode tests assert on the first verbose record, which is written before the store is touched, and the thumbprint test asks for
'NOTFOUND123', which no real certificate matches.The same fix closes a coverage gap
Get-PSBuildCertificate's post-load validation block has never executed. It is the block that gates a certificate loaded fromEnvVarorPfxFileon having a private key, not being expired, and carrying the Code Signing extended key usage:Every
EnvVarandPfxFiletest in the file feeds the function deliberately invalid data —[byte[]]@(1,2,3,4,5)base64-encoded, or an empty file with a.pfxextension — so construction of theX509Certificate2throws before the block is reached. Nothing produces a certificate object that survives to line 228.Code coverage over
Get-PSBuildCertificate.ps1with the whole file running:This is the validation that decides whether a certificate a consumer supplied through a CI secret is usable. It has never run.
Why it matters
Filters out expired certificatesandFilters out certificates without a private keyare the same test asReturns $null when no valid certificate is found: three identical assertions that the real store happens to be empty. Neither filter is exercised.The model to follow is already in the file
The
SkipValidation for store-backed sourcescontext added in #194 is written correctly:Nine tests there control what the store returns, and they are the only tests in the file that actually exercise the selection logic. Whatever is done here should look like those.
Options
-ModuleName 'PowerShellBuild'to the six mocks and give the three store tests real fixtures. Cheapest change, and it makesFilters out expired certificatesandFilters out certificates without a private keydo what their names say by returning an expired and a keyless certificate respectively. It also makes them deterministic everywhere.New-SelfSignedCertificate -Type CodeSigningCertproduces one with a private key and the Code Signing EKU, and it can be exported to a PFX under$TestDriveto drive thePfxFilesource, or base64-encoded into an environment variable to driveEnvVar. Windows-only, and it writes to the certificate store, so it needs a cleanup block. A certificate with-NotAfterin the past covers the expiry throw, and one created without the code-signing type covers the EKU throw.HasPrivateKey,NotAfter,Subject, andEnhancedKeyUsageList, so aPSCustomObjectwould exercise every branch — but only if the loading step can be intercepted, and it constructsX509Certificate2directly rather than through a mockable command. That would mean extracting the load behind a private function first.(1) and (2) together are the whole fix, and (1) alone is worth doing even if (2) is deferred, because it removes the machine dependency.
Related: #194 (the correctly scoped context), #197 (the platform guard on these same tests). Shares its mock-scoping defect and its remediation with #217, which has the same unscoped
Mockproblem intests/Invoke-PSBuildModuleSigning.tests.ps1.