Six mocks in the Get-PSBuildCertificate tests never reach the module, and three tests fail on a machine that has a code-signing certificate
Nobody has claimed this yet.
- Dominant language
- PowerShell
- Stars
- 145
- Forks
- 27
- Avg merge
- 10h 16m
- Merged PRs (30d)
- 34
Description
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 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 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 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. - Cover the post-load validation block by generating a real certificate.
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. - Cover the validation block with a fake object instead. The block only reads
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. - 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in tests/Get-PSBuildCertificate.tests.ps1 and compare the six store mocks with the correctly scoped mocks in the SkipValidation for store-backed sources context. Run the full test file, then make the store tests deterministic and exercise the post-load validation block for private keys, expiry, and Code Signing EKU. Done means the tests no longer depend on the local certificate store and the validation lines are covered.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- powershell
- Domain
- security, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100