Skip to content

v2026.04.11.2314

@rodchristiansen rodchristiansen tagged this 11 Apr 23:13
Introduces a tests/CryptEscrow.Tests/ project and wires `dotnet test`
into the CI workflow so PRs fail fast before publishing. Scaffolding
and critical-path coverage only; command-level tests are deferred to
a follow-up because they need a mocking layer over BitLocker / WMI.

## Framework choices

xUnit 2.9.2 + FluentAssertions 6.12.1 (pinned to the free version —
7.x moved to commercial) + RichardSzalay.MockHttp 7.0.0. No mocking
framework — 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).

TargetFramework: net10.0-windows (matches main project — uses
Windows-only APIs like Microsoft.Win32.Registry).

## Test seams in production code (minimal, non-behavioral)

- InternalsVisibleTo("CryptEscrow.Tests") in CryptEscrow.csproj.
- CryptServerClient gains an internal test ctor accepting an
  HttpMessageHandler so MockHttp can intercept without spinning up
  a real TLS stack. GetClientCertificate promoted from private to
  internal so strategy-ordering can be tested directly.
- ConfigService gains two internal static seams:
  ConfigPathOverride and RegistryReaderOverride. Both default to
  null; production behavior is unchanged.

## 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.
- TransientStoreCert: installs a cert in CurrentUser\My (no admin
  needed) and removes it on dispose.
- CredentialManagerWriter: test-only advapi32!CredWriteW helper for
  seeding entries that the production reader consumes. Uses DllImport
  (not LibraryImport) because the CREDENTIAL struct contains FILETIME
  which isn't compatible with DisableRuntimeMarshalling.

## Test coverage (39 tests, ~1s runtime)

- ConfigServiceTests (15): env > registry > YAML > default priority
  chain for every GetX() helper including the new PFX/PEM options;
  GetAuthConfig composition from mixed sources. Joined to a
  GlobalState collection with DisableParallelization = true to prevent
  static-state races.
- CredentialManagerTests (6): ASCII round trip, Unicode round trip,
  missing target, empty/null/whitespace target guard.
- GetClientCertificateTests (10): strategy priority ordering
  (CertStore > PFX > PEM), graceful fall-through on missing files,
  CertStoreWinsOverPfxWhenBothConfigured,
  FallsThroughToPfxWhenCertStoreThumbprintNotFound.
- CryptServerClientTests (8): checkin success/401/403 (no retry),
  form-url-encoded body fields, default and custom API key header,
  verify success/401.

## CI workflow

.github/workflows/ci.yml gains Restore, Test, and Upload test results
steps before the publish/package steps. TRX artifact uploaded via
actions/upload-artifact@v4. If tests fail, the workflow short-circuits
before producing an MSI.

## Copilot review resolution (addressed in-PR)

1. Test parallelization — fixed via GlobalState collection.
2. ConfigPathOverride edge cases (empty string, relative path) —
   fixed via shared ResolvedOverrideDir helper with IsNullOrWhiteSpace
   guards on both the override and its Path.GetDirectoryName result.
3. mTLS strategy ordering contradicted the README (PFX > PEM > store
   vs. store preferred in docs). Fixed by reordering code to match
   the security ranking. LoadFromCertStore short-circuits when neither
   thumbprint nor subject is set, so file-only users pay no cost and
   don't get misleading error logs. Added tests to prove the ordering.
4. README strategy ordering — addressed by the code fix; README
   already had the correct ordering.
5. AllowUnsafeBlocks — declined. It's required by the LibraryImport
   source generator (SYSLIB1062). Removing it breaks the build.

## Out of scope (follow-up PRs)

- Command-level tests for EscrowCommand / RotateCommand /
  VerifyCommand (need a mocking abstraction over BitLocker / WMI /
  manage-bde).
- Clock seam for retry backoff tests — current Task.Delay is real.
- Coverage threshold enforcement in CI (e.g. coverlet + 80% gate).
- Integration tests against a real Crypt Server.
- REG_DWORD handling bug in ConfigService.GetRegistryValue —
  tracked separately in #4.
Assets 2
Loading