scripts: vsql certificate script recovery paths from the second review
ci / frontend (push) Successful in 41s
ci / server (push) Successful in 11m2s
ci / image (push) Successful in 57s
ci / e2e (push) Failing after 4m25s

- A rollback record now says which restarts succeeded. A snapshot stays open
  while an instance is unrestored or its restart failed (or was declined), so
  -Rollback -RestartInstances retries exactly those instead of reporting done
  or stepping back to an older deployment.
- Running dependents (SQL Server Agent) are written to dependents-<service>.json
  before the stop and removed once they run again, so a failed restart does not
  leave Agent stopped for good.
- GetRegValue returns $null only for a value confirmed absent; a missing key or
  read error throws, so it is never snapshotted or verified as absent.
- Instances come from the value names of Instance Names\SQL; the 'PS*' filter
  dropped names such as PSREPORTING.

Tests for each, including the real registry operations on an HKCU test key.
Pester green on PowerShell 7.6 (63) and Windows PowerShell 5.1 (62, 1 skipped).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-18 21:50:34 -06:00
co-authored by Claude Opus 5.5
parent 90adcebc44
commit b7c22e1988
4 changed files with 204 additions and 39 deletions
+4 -3
View File
@@ -12,9 +12,10 @@ Run on a domain-joined PC. Exports every unexpired PESCO CA certificate from the
Run elevated on the SQL Server host behind vsql, in Windows PowerShell 5.1 or PowerShell 7. Requests a TLS certificate from PESCO-PSYLOCKE-CA with certreq the way SQL Server requires it (legacy Microsoft RSA SChannel Cryptographic Provider, KeySpec AT_KEYEXCHANGE, RSA 2048, SHA-256, Server Authentication, key usage 0xA0, SANs vsql.pesco.com, vsql and the host FQDN and short name). It installs it and validates it before anything is bound. The validation checks the key provider and KeySpec from the certificate's own key property, the key size, the signature, the dates, the EKU, and the SAN DNS names as whole names. It grants only the selected instances' service accounts read on the private key, then binds the thumbprint and reads it back.
- `-WhatIf` previews every step and changes nothing. `-Confirm` asks before each change. Declining enrollment or a key grant cancels the run with nothing bound (exit code 2).
- `-CerFile` continues a request that was pending. `-Thumbprint` binds a certificate already installed. `-Instances` limits the run (`DEV`, `MSSQLSERVER`). `-RestartInstances` restarts them and starts SQL Server Agent again if it ran.
- Each deployment writes a new `deploy-<time>.json` in `C:\ProgramData\PacketStation\vsql-cert` with the values it replaced. Instances already bound to the thumbprint are left alone and get no snapshot, so a rerun never hides the useful one. A pending request writes `request-<time>.json` with its request id.
- `-Rollback` restores the newest snapshot not yet rolled back, each instance on its own, verified by reading back, and records it in `rollback-<time>.json`. `-Snapshot` picks another. Failures are listed and the run exits with an error.
- `-CerFile` continues a request that was pending. `-Thumbprint` binds a certificate already installed. `-Instances` limits the run (`DEV`, `MSSQLSERVER`). `-RestartInstances` restarts them and starts SQL Server Agent again if it ran. The running dependents are written to `dependents-<service>.json` before the stop and the file is removed once they run again, so after a failed restart the next restart of that service starts them too.
- Each deployment writes a new `deploy-<time>.json` in `C:\ProgramData\PacketStation\vsql-cert` with the values it replaced. Instances already bound to the thumbprint are left alone and get no snapshot, so a rerun never hides the useful one. A pending request writes `request-<time>.json` with its request id. A registry value that cannot be read stops the run; only a value confirmed absent is saved as absent.
- `-Rollback` restores the newest snapshot not yet rolled back, each instance on its own, verified by reading back, and records it in `rollback-<time>.json` with which restarts succeeded. A snapshot stays open while an instance is unrestored or its restart failed, so `-Rollback -RestartInstances` again retries exactly those. `-Snapshot` picks another. Failures are listed and the run exits with an error.
- Instances are read from the value names of `Instance Names\SQL`, so a name starting with `PS` (for example `PSREPORTING`) is found.
Tests: `scripts/tests/Request-VsqlServerCertificate.Tests.ps1` (Pester 5) fakes the host (instances, services, registry, machine store, certreq, icacls) and uses real throwaway certificates in `Cert:\CurrentUser\My`. Run it on both PowerShell versions:
+79 -33
View File
@@ -329,18 +329,26 @@ function Get-VsqlDefaultProvider {
InstanceRoot = 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server'
IsAdmin = { ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) }
GetInstanceMap = {
# The key's own value names. Get-ItemProperty mixes in PSPath and friends, and
# filtering those by 'PS*' would drop a real instance such as PSREPORTING.
param($Root)
$p = Get-ItemProperty -LiteralPath "$Root\Instance Names\SQL" -ErrorAction SilentlyContinue
$path = "$Root\Instance Names\SQL"
$map = [ordered]@{}
if ($p) { foreach ($x in $p.PSObject.Properties) { if ($x.Name -notlike 'PS*') { $map[$x.Name] = [string]$x.Value } } }
if (Test-Path -LiteralPath $path) {
$key = Get-Item -LiteralPath $path -ErrorAction Stop
foreach ($n in $key.GetValueNames()) { if ($n) { $map[$n] = [string]$key.GetValue($n) } }
}
$map
}
GetService = { param($Name) Get-CimInstance Win32_Service -Filter (Get-ServiceFilter $Name) -ErrorAction Stop }
TestRegKey = { param($Key) Test-Path -LiteralPath $Key }
GetRegValue = {
# $null only for a value confirmed absent. A missing key or a read error throws, so it
# is never saved in a snapshot, or verified after a restore, as "absent".
param($Key, $Name)
$p = Get-ItemProperty -LiteralPath $Key -Name $Name -ErrorAction SilentlyContinue
if ($p) { [string]$p.$Name } else { $null }
$item = Get-Item -LiteralPath $Key -ErrorAction Stop
if (@($item.GetValueNames()) -notcontains $Name) { return $null }
[string]$item.GetValue($Name, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
}
SetRegValue = { param($Key, $Name, $Value) Set-ItemProperty -LiteralPath $Key -Name $Name -Value $Value -ErrorAction Stop -Confirm:$false }
RemoveRegValue = { param($Key, $Name) Remove-ItemProperty -LiteralPath $Key -Name $Name -ErrorAction Stop -Confirm:$false }
@@ -352,13 +360,9 @@ function Get-VsqlDefaultProvider {
GetKeyInfo = { param($Cert) Get-CertificateKeyInfo $Cert }
KeyFilePath = { param($KeyInfo) Join-Path $env:ProgramData "Microsoft\Crypto\RSA\MachineKeys\$($KeyInfo.UniqueName)" }
TestFile = { param($Path) Test-Path -LiteralPath $Path -PathType Leaf }
RestartService = {
param($Name)
$running = @((Get-Service -Name $Name).DependentServices | Where-Object { $_.Status -eq 'Running' })
Restart-Service -Name $Name -Force -ErrorAction Stop -Confirm:$false
foreach ($d in $running) { Start-Service -Name $d.Name -ErrorAction Stop -Confirm:$false }
@($running | ForEach-Object { $_.Name })
}
GetRunningDependents = { param($Name) @((Get-Service -Name $Name -ErrorAction Stop).DependentServices | Where-Object { $_.Status -eq 'Running' } | ForEach-Object { $_.Name }) }
RestartService = { param($Name) Restart-Service -Name $Name -Force -ErrorAction Stop -Confirm:$false }
StartService = { param($Name) Start-Service -Name $Name -ErrorAction Stop -Confirm:$false }
Now = { Get-Date }
Log = { param($Message) Write-Host $Message }
ShouldProcess = $null # $null: the cmdlet's own ShouldProcess (-WhatIf, -Confirm)
@@ -421,18 +425,36 @@ function Format-Value($Value) {
return $Value
}
function Get-DependentsFile($P, [string]$Service) {
return Join-Path $P.WorkDir ('dependents-' + ($Service -replace '[^A-Za-z0-9]', '_') + '.json')
}
function Invoke-InstanceRestart($P, $Confirm, [object[]]$Targets) {
# Restart-Service -Force stops dependents (SQL Server Agent) and does not start them again.
# The running ones are written to dependents-<service>.json before the stop, and the file is
# removed only once they run again. A failed restart therefore does not lose them: the next
# restart of that service, by a rerun or a rollback, starts them too.
$failed = New-Object System.Collections.Generic.List[string]
$restarted = New-Object System.Collections.Generic.List[string]
foreach ($t in $Targets) {
if (& $Confirm $t.Service 'restart service (and start its running dependents again)') {
try {
$deps = @(& $P.RestartService $t.Service)
$restarted.Add($t.Name)
& $P.Log "Restarted $($t.Service)$(if ($deps) { "; started again: $($deps -join ', ')" })"
if (-not (& $Confirm $t.Service 'restart service (and start its running dependents again)')) { continue }
try {
$file = Get-DependentsFile $P $t.Service
$saved = @()
if (Test-Path -LiteralPath $file) { $saved = @((Get-Content -LiteralPath $file -Raw | ConvertFrom-Json).services) }
$deps = @(Get-UniqueNames (@($saved) + @(& $P.GetRunningDependents $t.Service)))
if ($deps.Count) {
if (-not (Test-Path -LiteralPath $P.WorkDir)) { New-Item -ItemType Directory -Force -Path $P.WorkDir -Confirm:$false | Out-Null }
$json = ConvertTo-Json -InputObject ([ordered]@{ service = $t.Service; services = $deps; at = (& $P.Now).ToString('s') })
[System.IO.File]::WriteAllText($file, $json, (New-Object System.Text.UTF8Encoding $false))
}
catch { $failed.Add("$($t.Service): $($_.Exception.Message)") }
& $P.RestartService $t.Service
foreach ($d in $deps) { & $P.StartService $d }
if (Test-Path -LiteralPath $file) { Remove-Item -LiteralPath $file -Confirm:$false }
$restarted.Add($t.Name)
& $P.Log "Restarted $($t.Service)$(if ($deps.Count) { "; started again: $($deps -join ', ')" })"
}
catch { $failed.Add("$($t.Service): $($_.Exception.Message)") }
}
return [pscustomobject]@{ Restarted = $restarted.ToArray(); Failed = $failed.ToArray() }
}
@@ -447,20 +469,31 @@ function New-RecordPath($P, [string]$Prefix) {
throw "No free $Prefix record name in $($P.WorkDir)."
}
function Get-RolledBackInstances($P) {
# Snapshot file name -> instance names already restored from it, from the rollback records.
$done = @{}
foreach ($f in @(Get-ChildItem -LiteralPath $P.WorkDir -Filter 'rollback-*.json' -File -ErrorAction SilentlyContinue)) {
function Get-RollbackState($P) {
# Snapshot file name -> what the rollback records say: instances whose registry value was
# restored and verified, and instances whose restart was asked for but has not succeeded.
# Records are read in time order, so a later successful restart clears an earlier failure.
$state = @{}
foreach ($f in @(Get-ChildItem -LiteralPath $P.WorkDir -Filter 'rollback-*.json' -File -ErrorAction SilentlyContinue | Sort-Object Name)) {
$r = Get-Content -LiteralPath $f.FullName -Raw | ConvertFrom-Json
if (-not $done.ContainsKey($r.snapshot)) { $done[$r.snapshot] = @() }
$done[$r.snapshot] += @($r.instances)
if (-not $state.ContainsKey($r.snapshot)) {
$state[$r.snapshot] = [pscustomobject]@{
Restored = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
RestartPending = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
}
}
$s = $state[$r.snapshot]
foreach ($n in @($r.instances)) { if ($n) { [void]$s.Restored.Add($n) } }
foreach ($n in @($r.restarted)) { if ($n) { [void]$s.RestartPending.Remove($n) } }
foreach ($n in @($r.restartPending)) { if ($n) { [void]$s.RestartPending.Add($n) } }
}
return $done
return $state
}
function Invoke-VsqlRollback($P, $Confirm, [bool]$WhatIf, [string[]]$Instances, [string]$Snapshot, [bool]$RestartInstances) {
# Default: the newest snapshot with instances not yet restored, so each -Rollback steps back
# one deployment and a second run never re-applies a newer snapshot. -Snapshot picks one.
# Default: the newest snapshot with an instance not yet restored, or restored with a restart
# that did not succeed. Each -Rollback steps back one deployment, a retry finishes the one
# that failed, and a later run never re-applies a newer snapshot. -Snapshot picks one.
$files = @(Get-ChildItem -LiteralPath $P.WorkDir -Filter 'deploy-*.json' -File -ErrorAction SilentlyContinue | Sort-Object Name -Descending)
if (-not $files) { throw "No deployment snapshots in $($P.WorkDir)." }
$load = { param($f) Get-Content -LiteralPath $f.FullName -Raw | ConvertFrom-Json }
@@ -479,11 +512,11 @@ function Invoke-VsqlRollback($P, $Confirm, [bool]$WhatIf, [string[]]$Instances,
$entries = @(& $pick $chosen.Data)
}
else {
$done = Get-RolledBackInstances $P
$state = Get-RollbackState $P
foreach ($f in $files) {
$s = & $load $f
$restoredBefore = if ($done.ContainsKey($f.Name)) { @($done[$f.Name]) } else { @() }
$open = @(& $pick $s | Where-Object { $restoredBefore -notcontains $_.name })
$st = $state[$f.Name]
$open = @(& $pick $s | Where-Object { -not $st -or -not $st.Restored.Contains($_.name) })
if ($open) { $chosen = [pscustomobject]@{ File = $f; Data = $s }; $entries = $open; break }
}
if (-not $chosen) {
@@ -508,13 +541,26 @@ function Invoke-VsqlRollback($P, $Confirm, [bool]$WhatIf, [string[]]$Instances,
& $P.Log "Restored $($e.name) to $(Format-Value $prev) (verified)"
}
}
if ($restored.Count) {
Write-NewJsonFile (New-RecordPath $P 'rollback') ([ordered]@{ at = (& $P.Now).ToString('s'); snapshot = $chosen.File.Name; instances = @($restored | ForEach-Object { $_.Name }) })
}
$restart = [pscustomobject]@{ Restarted = @(); Failed = @() }
if ($restored.Count) {
if ($RestartInstances) { $restart = Invoke-InstanceRestart $P $Confirm $restored.ToArray() }
$names = @($restored | ForEach-Object { $_.Name })
if ($RestartInstances) {
try { $restart = Invoke-InstanceRestart $P $Confirm $restored.ToArray() }
finally {
# Written after the restarts and even if they throw. A restored instance that was not
# restarted stays open, so the next -Rollback -RestartInstances retries it.
$done = @($restart.Restarted)
Write-NewJsonFile (New-RecordPath $P 'rollback') ([ordered]@{
at = (& $P.Now).ToString('s')
snapshot = $chosen.File.Name
instances = $names
restarted = $done
restartPending = @($names | Where-Object { $done -notcontains $_ })
})
}
}
else {
Write-NewJsonFile (New-RecordPath $P 'rollback') ([ordered]@{ at = (& $P.Now).ToString('s'); snapshot = $chosen.File.Name; instances = $names; restarted = @(); restartPending = @() })
& $P.Log 'Restart the restored instances for the rollback to take effect:'
foreach ($r in $restored) { & $P.Log " Restart-Service -Name '$($r.Service)' -Force" }
}
@@ -84,6 +84,10 @@ BeforeAll {
IcaclsFail = $false
SetFails = @{} # regKey -> $true: Set-ItemProperty throws
SetIgnored = @{} # regKey -> $true: the write does not stick
ReadFails = @{} # regKey -> $true: reading throws (access denied)
Dependents = @{ 'MSSQLSERVER' = @('SQLSERVERAGENT'); 'MSSQL$DEV' = @('SQLAgent$DEV') }
Running = New-Object 'System.Collections.Generic.HashSet[string]' ([StringComparer]::OrdinalIgnoreCase)
RestartFails = @{} # service -> $true: stops it and its dependents, then fails to start
KeyFile = Join-Path $Work 'machinekey.bin'
ShouldProcess = $null
}
@@ -97,7 +101,11 @@ BeforeAll {
GetInstanceMap = { param($r) $fake.Instances }.GetNewClosure()
GetService = { param($n) [void]$fake.Calls.Add("GetService $n"); $fake.Services[$n] }.GetNewClosure()
TestRegKey = { param($k) $fake.Registry.ContainsKey($k) }.GetNewClosure()
GetRegValue = { param($k, $n) $v = $fake.Registry[$k]; if ($v.ContainsKey($n)) { [string]$v[$n] } else { $null } }.GetNewClosure()
GetRegValue = {
param($k, $n)
if ($fake.ReadFails[$k]) { throw "Requested registry access is not allowed: $k" }
$v = $fake.Registry[$k]; if ($v.ContainsKey($n)) { [string]$v[$n] } else { $null }
}.GetNewClosure()
SetRegValue = {
param($k, $n, $v)
[void]$fake.Calls.Add("SetReg $k $n=$v")
@@ -139,11 +147,21 @@ BeforeAll {
GetKeyInfo = { param($c) $k = & $realKeyInfo $c; if ($k) { $k.Machine = $true }; $k }.GetNewClosure()
KeyFilePath = { param($k) $fake.KeyFile }.GetNewClosure()
TestFile = { param($f) Test-Path -LiteralPath $f -PathType Leaf }
RestartService = { param($n) [void]$fake.Calls.Add("Restart $n"); @() }.GetNewClosure()
GetRunningDependents = { param($n) @($fake.Dependents[$n] | Where-Object { $fake.Running.Contains($_) }) }.GetNewClosure()
RestartService = {
param($n)
[void]$fake.Calls.Add("Restart $n")
foreach ($d in $fake.Dependents[$n]) { [void]$fake.Running.Remove($d) }
[void]$fake.Running.Remove($n)
if ($fake.RestartFails[$n]) { throw "Service '$n' cannot be started" }
[void]$fake.Running.Add($n)
}.GetNewClosure()
StartService = { param($n) [void]$fake.Calls.Add("Start $n"); [void]$fake.Running.Add($n) }.GetNewClosure()
Now = { Get-Date }
Log = { param($m) [void]$fake.Log.Add("$m") }.GetNewClosure()
ShouldProcess = $null
}
foreach ($svc in 'MSSQLSERVER', 'MSSQL$DEV', 'SQLSERVERAGENT', 'SQLAgent$DEV') { [void]$fake.Running.Add($svc) }
$fake.Provider = $p
return $fake
}
@@ -547,6 +565,106 @@ Describe 'Finding 6: rollback restores each instance on its own and verifies' {
}
}
Describe 'Second review: recovery paths' {
BeforeEach {
$f = New-FakeHost (Join-Path $TestDrive ([guid]::NewGuid()))
Add-ToStore $f $Good
$f.Registry[$KeyDefault].Certificate = 'aaaa'; $f.Registry[$KeyDev].Certificate = 'bbbb'
Invoke-VsqlCertificateDeployment -Provider $f.Provider -Thumbprint $Good.Thumbprint | Out-Null
$f.Calls.Clear()
}
It 'a rollback whose restart failed is retried, not skipped and not stepped past' {
$f.RestartFails['MSSQL$DEV'] = $true
{ Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances } | Should -Throw '*Rollback incomplete*MSSQL$DEV*'
$f.Registry[$KeyDev].Certificate | Should -Be 'bbbb'
$f.RestartFails.Clear()
$f.Calls.Clear()
$r = Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances
$r.Snapshot | Should -Not -BeNullOrEmpty
$r.Restarted | Should -Be @('DEV')
$f.Calls | Should -Contain 'Restart MSSQL$DEV'
$f.Calls | Should -Not -Contain 'Restart MSSQLSERVER'
$f.Registry[$KeyDev].Certificate | Should -Be 'bbbb'
# Now complete: nothing left to do.
(Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances).Snapshot | Should -BeNullOrEmpty
}
It 'a declined restart under -Confirm also stays open' {
$f.Provider.ShouldProcess = { param($t, $a) $a -notlike 'restart service*' -or $t -ne 'MSSQL$DEV' }
Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances | Out-Null
$f.Provider.ShouldProcess = $null
$r = Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances
$r.Restarted | Should -Be @('DEV')
}
It 'SQL Server Agent is started again after a failed restart is retried' {
$f.RestartFails['MSSQL$DEV'] = $true
{ Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances } | Should -Throw
$f.Running.Contains('SQLAgent$DEV') | Should -BeFalse
$depFile = Join-Path $f.Provider.WorkDir 'dependents-MSSQL_DEV.json'
(Get-Content $depFile -Raw | ConvertFrom-Json).services | Should -Be @('SQLAgent$DEV')
$f.RestartFails.Clear()
Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback -RestartInstances | Out-Null
$f.Calls | Should -Contain 'Start SQLAgent$DEV'
$f.Running.Contains('SQLAgent$DEV') | Should -BeTrue
Test-Path $depFile | Should -BeFalse
}
It 'a successful restart starts the agent that ran and leaves no dependents file' {
Invoke-VsqlCertificateDeployment -Provider $f.Provider -Thumbprint $Good.Thumbprint -Instances MSSQLSERVER -RestartInstances | Out-Null
$f.Calls | Should -Contain 'Start SQLSERVERAGENT'
$f.Running.Contains('SQLSERVERAGENT') | Should -BeTrue
@(Get-ChildItem $f.Provider.WorkDir -Filter 'dependents-*.json') | Should -BeNullOrEmpty
}
It 'a registry read failure stops a deployment before any snapshot or change' {
Add-ToStore $f $Good2
$before = @(Get-Snapshots $f).Count
$f.ReadFails[$KeyDev] = $true
{ Invoke-VsqlCertificateDeployment -Provider $f.Provider -Thumbprint $Good2.Thumbprint } | Should -Throw '*not allowed*'
@($f.Calls | Where-Object { $_ -match '^(SetReg|RemoveReg)' }) | Should -BeNullOrEmpty
@(Get-Snapshots $f).Count | Should -Be $before
}
It 'a registry read failure during rollback is a failure, never verified as absent' {
$f.ReadFails[$KeyDev] = $true
{ Invoke-VsqlCertificateDeployment -Provider $f.Provider -Rollback } | Should -Throw '*Rollback incomplete*DEV*not allowed*'
$f.Log | Should -Not -Contain 'Restored DEV to bbbb (verified)'
}
}
Describe 'Real registry operations (HKCU test key)' {
BeforeAll {
$script:TestRoot = "HKCU:\Software\PS2-VSQL-CERT-TEST-$([guid]::NewGuid())"
New-Item -Path "$TestRoot\Instance Names\SQL" -Force | Out-Null
New-ItemProperty -Path "$TestRoot\Instance Names\SQL" -Name 'PSREPORTING' -Value 'MSSQL16.PSREPORTING' | Out-Null
New-ItemProperty -Path "$TestRoot\Instance Names\SQL" -Name 'DEV' -Value 'MSSQL16.DEV' | Out-Null
New-Item -Path "$TestRoot\Empty" -Force | Out-Null
New-ItemProperty -Path "$TestRoot\Empty" -Name 'Certificate' -Value '' | Out-Null
New-Item -Path "$TestRoot\Absent" -Force | Out-Null
$script:Real = Get-VsqlDefaultProvider
}
AfterAll { Remove-Item -LiteralPath $TestRoot -Recurse -Force }
It 'lists every instance, including one whose name starts with PS' {
$map = & $Real.GetInstanceMap $TestRoot
@($map.Keys) | Should -Be @('PSREPORTING', 'DEV')
$map['PSREPORTING'] | Should -Be 'MSSQL16.PSREPORTING'
}
It 'returns an empty map when no instance is registered' {
(& $Real.GetInstanceMap "$TestRoot\Absent").Count | Should -Be 0
}
It 'tells an empty value from an absent one' {
& $Real.GetRegValue "$TestRoot\Empty" 'Certificate' | Should -BeExactly ''
$null -eq (& $Real.GetRegValue "$TestRoot\Absent" 'Certificate') | Should -BeTrue
}
It 'throws, rather than reporting absent, when the key cannot be read' {
{ & $Real.GetRegValue "$TestRoot\Missing" 'Certificate' } | Should -Throw
}
It 'writes, reads back and removes through the real operations' {
& $Real.SetRegValue "$TestRoot\Absent" 'Certificate' 'abcd'
& $Real.GetRegValue "$TestRoot\Absent" 'Certificate' | Should -Be 'abcd'
& $Real.RemoveRegValue "$TestRoot\Absent" 'Certificate'
$null -eq (& $Real.GetRegValue "$TestRoot\Absent" 'Certificate') | Should -BeTrue
}
}
Describe '-WhatIf' {
It 'changes nothing on the real ShouldProcess path' {
$f = New-FakeHost (Join-Path $TestDrive 'whatif-real')