Closes #4.
GetRegistryValue was reading raw values with `key?.GetValue(name) as string`,
which returns null for any non-string registry type. REG_DWORD values come
back from RegistryKey.GetValue as boxed int — so every CSP-deployed
boolean or integer policy that used the standard DWORD wire format was
silently dropped. This affected GetUseMtls, GetAutoRotate,
GetSkipCertCheck, GetCleanupOldProtectors, GetKeyEscrowIntervalHours, and
GetValidateKey — all documented as String/DWORD in the README but only
actually working for strings. The "Handle registry DWORD values"
fall-through in GetRegistryBool (ConfigService.cs:115) was unreachable.
## Fix
Factor out ConvertToConfigString as an internal pure helper that
converts a raw registry value to its string form, and route both
production paths (GP and MDM) through it:
- string / REG_SZ / REG_EXPAND_SZ -> returned as-is (null for empty)
- int / REG_DWORD -> invariant-culture int.ToString
- long / REG_QWORD -> invariant-culture long.ToString
- anything else (REG_BINARY, REG_MULTI_SZ, ...) -> null (caller falls through)
GetRegistryBool and GetRegistryInt now receive the string form of DWORDs
and the existing int.TryParse branches light up.
## Tests (22 new, 61 total passing in ~1s)
RegistryValueConversionTests:
- 8 pure unit tests on ConvertToConfigString covering null, empty/whitespace
strings, positive/negative/zero/MaxValue/MinValue DWORDs, QWORD max,
byte arrays and string arrays (both null).
- 3 integration tests that open a real HKCU subkey via TempRegistryKey,
call GetValue on REG_SZ/REG_DWORD/REG_QWORD entries, and assert
ConvertToConfigString returns the expected string. These prove the
boxed int/long round trip works against actual registry data — the
part of the production chain that the old `as string` cast dropped.
- 4 end-to-end tests (GetSkipCertCheck, GetKeyEscrowIntervalHours,
GetAutoRotate) exercising the full GetRegistryBool/Int path with
DWORD values.
## Test seam changes
TempRegistryKey.ReadValue now delegates to ConfigService.ConvertToConfigString
instead of having its own private switch, so production and test conversion
logic can never drift apart. New TempRegistryKey.OpenKey() returns the
underlying HKCU key so tests can exercise the raw GetValue chain directly.
TempRegistryKey.SetQword added for QWORD coverage.
## Copilot review (addressed in-PR)
1. Serilog property placeholder {Path} was colliding with the codebase
convention of using {Path} for real filesystem/registry paths.
Renamed to {PolicySource} in ReadValueFromPath's log lines.
2. GetRegistryValue XML doc summary still said "Reads a string value"
even though the method now accepts DWORD/QWORD. Reworded to "Reads
an enterprise policy value ... and returns its string representation".