SQL Server 2025 renamed a certificate type and my green test suite never noticed

SqlCertForge · September 2026 · 7 min read

I ship a PowerShell tool that manages certificates on SQL Server — TLS bindings, TDE, backup encryption, Always Encrypted column master keys. Version 2 grew the surface from 19 commands to 40, and before release every state-changing command had to run against real servers: SQL Server 2022, SQL Server 2025, Reporting Services, Power BI Report Server.

Going into that lab pass I felt good. The unit suites were green on both Windows PowerShell 5.1 and PowerShell 7 — Pester on the PowerShell side, xUnit on the C# side. Green twice over.

Then, partway through adding SQL Server 2025 to the lab, the TDE check told me a database I had encrypted that morning had no certificate.

I ran it again. Same answer. I queried the server by hand — the certificate sat right there, healthy, doing its job, and the database was demonstrably encrypted. The tool kept insisting there was nothing to see. That specific flavor of wrong is maddening, because for a while you believe the server is lying to you. It wasn’t. My code was, politely, and with a green test suite behind it.

The hunt

The cause took longer to find than it deserved, because nothing was failing.

Every suspect checked out innocent, in order. The tool’s own inventory query, run by hand against the 2025 instance: returns the certificate. So the query is fine. The rows coming back through the tool’s data layer: certificate’s in there, I can see the object in the pipeline. So the plumbing is fine. The certificate survived every single hop — and then one Where-Object made it vanish, and a filter that returns nothing looks exactly like an estate that contains nothing. No exception, no warning, no red text. Just quietly less data than reality, somewhere downstream of everything that had just proven itself correct.

By then the only suspect left standing was the comparison itself. The certificate went into that filter and didn’t come out, which means the -eq evaluated false, which means one side of it wasn’t what I believed it was. There’s exactly one way to settle an argument like that: stop reading code and go look at the actual value. So I dumped the raw certificate row from the 2022 instance and the same certificate’s row from 2025, put them side by side, and read every property. Name — same. Expiry — same. Key backup state — same. Type — CERTIFICATE on one. CERTIFICATE_OAEP_256 on the other.

The reveal made my head hurt, and I wanted to cry. Nineteen characters I had read past a dozen times, because when you’re hunting a bug you read the properties you suspect, and nobody suspects the type name of a certificate of changing between versions.

The rename

It had, though, and for a defensible reason: SQL Server 2025’s type name carries the padding scheme. Stronger padding, and the name tells you which. Nobody’s upgrade broke; encryption kept working.

What broke was every filter in my code shaped like this:

$certs | Where-Object { $_.Type -eq 'CERTIFICATE' }

On SQL Server 2025 that filter returns nothing. Not an error — nothing. And in a tool whose job includes answering “is TDE protected by a backed-up certificate?”, an empty result doesn’t look like a bug. It looks like an answer: no certificate here.

Then came the cleanup, with its own little joke at my expense: finding every place that compares that string means searching the codebase for the word CERTIFICATE — in a tool that is entirely about certificates. A wall of hits, nearly all of them innocent. Four call sites carried the filter, each one quietly concluding a real, present certificate didn’t exist. A user on SQL Server 2025 would have been told their encrypted database had nothing to escrow — while it sat right there.

Why the green suite never saw it coming

If you haven’t lived in PowerShell: Pester is its test framework, and Pester’s Mock command replaces any command with a stub that returns whatever you script. It’s a genuinely great tool — fast, surgical, and the right way to test logic without a SQL Server in the room.

My tests mock the one seam where queries leave the tool:

Mock Invoke-SqlCertInventoryQuery { @( @{ Name = 'TDECert'; Type = 'CERTIFICATE' } ) }

See the problem? The mocked row says Type = 'CERTIFICATE' because that’s what SQL Server had always returned — that’s what I knew. So the tests proved, rigorously and repeatably, that my filter matches the string my mock produces. The mock and the assertion were the same belief written twice, and a loop like that can never contain a surprise. Bugs are made of surprises.

The only place CERTIFICATE_OAEP_256 existed was an actual SQL Server 2025 instance. So the only test that could fail was the one that talked to it.

The code fix was tiny, but it changed how I write this kind of check. I had to stop treating the type as a constant and read it as a value that can change — match the certificate encryptor family rather than one exact string, so the code adapts to names it hasn’t met yet. And I had to rewrite the tests, because the old ones were part of the problem: the new ones deliberately feed the check values it doesn’t expect, including the 2025 string, instead of handing it its own answer. A very small amount of code, a bigger change in posture, and a lab re-run proving the same check passes on 2022 and 2025 against the same certificate.

It wasn’t alone

The first real lab bug does something useful: it ruins your faith in the green, and that turns out to be healthy. Once I stopped trusting the suite and started hunting, five more fell out — different causes, same shape, every one living at a boundary the unit tests had mocked away:

None of the six ever produced a wrong answer in a mocked run. All six produced wrong behavior against a real server within a day of lab time.

What the lab pass actually proved

So what replaced the misplaced faith? Not clicking around until things looked right. The validation pass proved the tool’s actual promises end to end:

Notice what those have in common: each proof contains a designed failure. The backup that refuses to restore is the point — if it had restored without the certificate, the feature would be meaningless. A validation pass that can’t fail isn’t validating anything.

Honest scope

This is one person’s suite and one lab, not a CI philosophy for a hundred-engineer org. The Pester suites stayed — they’re fast, they pin logic, and they catch regressions in the code I control. dbatools and Pester are load-bearing priors here, not things I invented around.

What changed is where the trust goes. A green mocked suite now earns the claim “my logic matches my beliefs.” Only the live pass earns “my beliefs match SQL Server.” Those used to be one checkbox in my head; the morning a freshly encrypted database came back “no certificate,” they split permanently.

The rule I took away: for any check that gates on an external system, there must be at least one test where the real-world value can vary — a live run, or a test that deliberately feeds the check something other than its own expected value. If every input is the expected value, the test is a tautology with good branding.

What’s the CERTIFICATE_OAEP_256 in your stack — the value a vendor renamed, a column that moved, a default that flipped — that your mocks are still returning from memory?

← All posts