Add xUnit test suite covering ConfigService, CryptServerClient, and cert strategies#3
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new xUnit-based test suite for CryptEscrow (config precedence, HTTP client behavior, Credential Manager, and mTLS cert strategy selection) and wires dotnet test into CI, plus introduces a few small internal seams to make the production code testable.
Changes:
- Add
tests/CryptEscrow.Teststest project with fixtures and ~37 unit tests. - Add CI steps to run
dotnet testand upload TRX results. - Introduce test seams/internal access in
ConfigServiceandCryptServerClient(andInternalsVisibleTo), plus add Credential Manager support.
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
tests/CryptEscrow.Tests/Services/ConfigServiceTests.cs |
Tests config priority chain + GetAuthConfig composition. |
tests/CryptEscrow.Tests/Services/CryptServerClientTests.cs |
Verifies HTTP behavior via injected HttpMessageHandler. |
tests/CryptEscrow.Tests/Services/CredentialManagerTests.cs |
Exercises CredentialManager.ReadGenericPassword behavior. |
tests/CryptEscrow.Tests/Services/GetClientCertificateTests.cs |
Verifies mTLS client-cert strategy selection and fall-through. |
tests/CryptEscrow.Tests/Fixtures/* |
Env/registry/config/cert/credential test isolation helpers. |
tests/CryptEscrow.Tests/CryptEscrow.Tests.csproj |
Adds xUnit + FluentAssertions + MockHttp dependencies. |
src/CryptEscrow/Services/ConfigService.cs |
Adds internal test seams + new auth getters for PEM/PFX. |
src/CryptEscrow/Services/CryptServerClient.cs |
Adds internal test constructor + exposes/expands cert loading strategies. |
src/CryptEscrow/Services/CredentialManager.cs |
Adds Credential Manager reader used by PFX strategy. |
src/CryptEscrow/CryptEscrow.csproj |
Adds InternalsVisibleTo and enables unsafe blocks. |
.github/workflows/ci.yml |
Runs tests on CI and uploads TRX artifacts. |
README.md |
Documents new mTLS configuration options and env/registry keys. |
CryptEscrow.sln |
Adds test project to solution. |
.gitignore |
Ignores test result directories. |
Comments suppressed due to low confidence (1)
src/CryptEscrow/Services/ConfigService.cs:66
- GetRegistryValue currently reads registry data using
key?.GetValue(valueName) as string, which silently drops non-string policy values (e.g., DWORDs). This breaks the documented String/DWORD registry support (and means GetRegistryBool/GetRegistryInt won’t work for DWORD values in real HKLM policy keys). Consider switching tovar raw = key?.GetValue(valueName)and handlingstring+ numeric types consistently (e.g.,int,long) before returning.
// Try standard Group Policy path first
using var key = Registry.LocalMachine.OpenSubKey(RegistryBasePath);
var value = key?.GetValue(valueName) as string;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…hardening Three fixes from Copilot's review of PR #3: 1. Cert strategy priority now matches the documented security ranking. GetClientCertificate tried PFX -> PEM -> Cert Store, but the README (and a contradicting comment in the same file) described Cert Store as preferred/most secure. Users who configured both a Cert Store thumbprint and a PFX file would silently get the less-secure option. New order: Cert Store -> PFX -> PEM. Cert Store lookup short-circuits when neither thumbprint nor subject is set, so file-only users pay no cost and don't get misleading error logs. Adds two tests: CertStoreWinsOverPfxWhenBothConfigured (uses TransientStoreCert to install a cert in CurrentUser\My without admin) and FallsThroughToPfxWhenCertStoreThumbprintNotFound. 2. ConfigService test seams harden against empty/whitespace/relative ConfigPathOverride values. Previously an empty string caused an inconsistent state (ConfigDir fell back, ConfigPath didn't) and a relative filename would have non-null-but-empty GetDirectoryName results. Now a shared ResolvedOverrideDir helper gates all three path accessors consistently; invalid overrides fall back to defaults. No production code path sets this, so behavior is unchanged in production. 3. ConfigServiceTests joins a new GlobalState collection (DisableParallelization = true) since it mutates process-wide env vars and ConfigService static overrides. xUnit runs test classes in parallel by default — no flakes today because this is the only class touching that state, but the annotation is defensive insurance against future test additions. Not fixed: Copilot's suggestion to remove AllowUnsafeBlocks from CryptEscrow.csproj. That flag is required by the LibraryImport source generator used in CredentialManager.cs (SYSLIB1062). Removing it breaks the build. Tests: 39 passed, 0 failed, 0 skipped in ~1s.
Copilot review — responseAddressed four of the five comments in 917376e. Summary:
New tests added
Test count: 37 → 39, all passing locally in ~1s. Suppressed low-confidence commentCopilot also flagged a pre-existing issue in |
…ert strategies
Introduces a tests/CryptEscrow.Tests/ project and wires `dotnet test` into
the CI workflow. This is the scaffolding + critical-path coverage pass;
command-level tests (EscrowCommand, RotateCommand, VerifyCommand) are
deferred to a follow-up because they need a mocking layer over BitLocker /
WMI.
Framework choice: xUnit + FluentAssertions + RichardSzalay.MockHttp. No
mocking framework — the code under test is mostly static classes, concrete
types, and file I/O, so ambient-state fixtures + InternalsVisibleTo are
sufficient (and we avoid the Moq SponsorLink controversy).
Main project changes to enable testing:
- InternalsVisibleTo("CryptEscrow.Tests") on CryptEscrow.csproj.
- CryptServerClient gets an internal test ctor that accepts an
HttpMessageHandler directly, so MockHttp can intercept traffic without
spinning up a real TLS stack. GetClientCertificate is promoted from
private to internal so strategy-ordering can be tested directly.
- ConfigService gains two test seams — ConfigPathOverride and
RegistryReaderOverride — that let fixtures redirect YAML and registry
reads to per-test temp locations without admin rights. Both default to
null; production behavior is unchanged.
Test fixtures (tests/CryptEscrow.Tests/Fixtures/):
- EnvironmentSnapshot: scoped env-var save/restore.
- TempRegistryKey: creates HKCU\Software\CryptEscrowTest\<guid>, installs
the reader override, deletes on dispose.
- TempConfigFile: writes YAML to a unique %TEMP% subdir and points
ConfigPathOverride at it.
- SelfSignedCertFactory: generates ephemeral RSA certs via
CertificateRequest and serializes to PEM/key and PFX temp files.
- CredentialManagerWriter: test-only advapi32!CredWriteW helper for
seeding Credential Manager entries that the production reader can then
consume. Uses DllImport (not LibraryImport) because the CREDENTIAL
struct contains FILETIME which isn't compatible with
DisableRuntimeMarshalling.
Tests (37 total, all pass locally in ~800ms):
- ConfigServiceTests (15): priority chain for GetApiKey, GetUseMtls,
GetCertificateSubject, GetClientCertPath/KeyPath, GetPfxPath,
GetPfxPasswordCredential; GetAuthConfig composition from mixed sources.
- CredentialManagerTests (6): ASCII round trip, unicode round trip,
missing target, empty/null/whitespace target guard.
- GetClientCertificateTests (8): PFX wins over PEM, PEM-only loads,
unprotected PFX loads, missing credential falls through to PEM,
missing PFX file falls through to PEM, missing PEM files return null,
no config returns null.
- CryptServerClientTests (8): checkin success/401/403 (no retry), form
body fields, default and custom API key header, verify success/401.
CI workflow:
- New Restore/Test/Upload-test-results steps in .github/workflows/ci.yml
run before the publish/package steps, so a broken PR fails fast without
producing an MSI.
- TRX artifact uploaded via actions/upload-artifact@v4.
.gitignore adds TestResults/ exclusion.
Out of scope (later PRs):
- Command-level tests (need BitLocker/WMI mocking abstraction).
- Clock seam for retry backoff tests (current Task.Delay is real).
- Coverage threshold enforcement in CI.
- Integration tests against a real Crypt Server.
…hardening Three fixes from Copilot's review of PR #3: 1. Cert strategy priority now matches the documented security ranking. GetClientCertificate tried PFX -> PEM -> Cert Store, but the README (and a contradicting comment in the same file) described Cert Store as preferred/most secure. Users who configured both a Cert Store thumbprint and a PFX file would silently get the less-secure option. New order: Cert Store -> PFX -> PEM. Cert Store lookup short-circuits when neither thumbprint nor subject is set, so file-only users pay no cost and don't get misleading error logs. Adds two tests: CertStoreWinsOverPfxWhenBothConfigured (uses TransientStoreCert to install a cert in CurrentUser\My without admin) and FallsThroughToPfxWhenCertStoreThumbprintNotFound. 2. ConfigService test seams harden against empty/whitespace/relative ConfigPathOverride values. Previously an empty string caused an inconsistent state (ConfigDir fell back, ConfigPath didn't) and a relative filename would have non-null-but-empty GetDirectoryName results. Now a shared ResolvedOverrideDir helper gates all three path accessors consistently; invalid overrides fall back to defaults. No production code path sets this, so behavior is unchanged in production. 3. ConfigServiceTests joins a new GlobalState collection (DisableParallelization = true) since it mutates process-wide env vars and ConfigService static overrides. xUnit runs test classes in parallel by default — no flakes today because this is the only class touching that state, but the annotation is defensive insurance against future test additions. Not fixed: Copilot's suggestion to remove AllowUnsafeBlocks from CryptEscrow.csproj. That flag is required by the LibraryImport source generator used in CredentialManager.cs (SYSLIB1062). Removing it breaks the build. Tests: 39 passed, 0 failed, 0 skipped in ~1s.
917376e to
a6bd768
Compare
Consolidates the four PRs merged today into a single semver entry for humans, and triggers a new CI release whose body will link back to the individual timestamp-tagged releases (2308, 2314, 2344, 2347). The entry credits @aysiu explicitly for the PEM client cert contribution in PR #2, which was the original spark for the mTLS feature. PFX + Credential Manager and the strategy-priority refactor were layered on as maintainer tweaks. Also documents: - xUnit test suite + dotnet test CI integration (PR #3) - Node.js 24 opt-in for JavaScript Actions (PR #5) - REG_DWORD/QWORD handling fix in GetRegistryValue, closing #4 (PR #6)
Summary
tests/CryptEscrow.Tests/— an xUnit test project coveringConfigServicepriority chains,CryptServerClientHTTP behavior,CredentialManagerround-trip, and the mTLS cert-strategy ordering added in Allow for .pem and .key client cert authentication #2.dotnet testinto.github/workflows/ci.ymlso PRs fail fast before publishing.Note
Depends on #2 — this branch is based on the state of
aysiu-patch-1so the new tests can compile against the PFX / PEM / Credential Manager code added there. Until #2 merges, the PR diff will include aysiu's changes too; once #2 lands, a rebase againstmainwill shrink the diff to just the test additions and the small production-code seams below.Production code changes (minimal, non-behavioral)
src/CryptEscrow/CryptEscrow.csproj:<InternalsVisibleTo Include=\"CryptEscrow.Tests\" />.src/CryptEscrow/Services/CryptServerClient.cs:internaltest constructor accepting anHttpMessageHandlerdirectly, soRichardSzalay.MockHttpcan intercept traffic without a real TLS stack.GetClientCertificatepromoted fromprivatetointernalso strategy ordering can be tested directly.src/CryptEscrow/Services/ConfigService.cs:internal static string? ConfigPathOverride— redirects YAML reads to per-test temp files.internal static Func<string, string?>? RegistryReaderOverride— redirects registry reads to per-testHKCUsubkeys (tests run without admin).null. Production behavior is unchanged.Test coverage (37 tests)
ConfigServiceTestsGetApiKey,GetUseMtls,GetCertificateSubject,GetClientCertPath/KeyPath,GetPfxPath,GetPfxPasswordCredential;GetAuthConfigcomposition from mixed sources.CredentialManagerTestsGetClientCertificateTestsCryptServerClientTestsX-API-Keyheader, verify success / 401.Test fixtures (
tests/CryptEscrow.Tests/Fixtures/)EnvironmentSnapshot— scoped env-var save/restore.TempRegistryKey— createsHKCU\Software\CryptEscrowTest\<guid>, installs the reader override, deletes on dispose.TempConfigFile— writes YAML to a unique%TEMP%subdir and pointsConfigPathOverrideat it.SelfSignedCertFactory— generates ephemeral RSA certs viaCertificateRequestand serializes to PEM/key and PFX temp files.CredentialManagerWriter— test-onlyadvapi32!CredWriteWhelper for seeding entries that the production reader consumes. UsesDllImport(notLibraryImport) because theCREDENTIALstruct containsFILETIME, which isn't compatible withDisableRuntimeMarshalling.Framework choices
InternalsVisibleToare sufficient (and we avoid the Moq SponsorLink controversy).TargetFramework: net10.0-windows(must match main project — uses Windows-only APIs likeMicrosoft.Win32.Registry).CI updates
Runs before the existing publish/package steps. If tests fail, the workflow short-circuits before producing an MSI.
Out of scope (follow-up PRs)
EscrowCommand/RotateCommand/VerifyCommand— need a mocking abstraction over BitLocker / WMI /manage-bde.Task.Delaycalls are real.Test plan
dotnet build CryptEscrow.sln— zero warnings, zero errorsdotnet test CryptEscrow.sln -c Release— 37 passed, 0 failed, ~800msConfigService.GetApiKey— the corresponding test fails (proves the test actually exercises the code)test-results.trxmainand confirm the diff shrinks to just the test additions