583 lines
22 KiB
PowerShell
583 lines
22 KiB
PowerShell
$ErrorActionPreference = "Stop"
|
|
|
|
$supportPath = Join-Path $PSScriptRoot "MinerOpsNativeHarnessSupport.ps1"
|
|
. $supportPath
|
|
|
|
function Assert-True {
|
|
param(
|
|
[Parameter(Mandatory = $true)][bool]$Condition,
|
|
[Parameter(Mandatory = $true)][string]$Message
|
|
)
|
|
if (-not $Condition) { throw $Message }
|
|
}
|
|
|
|
function Assert-Throws {
|
|
param(
|
|
[Parameter(Mandatory = $true)][scriptblock]$Action,
|
|
[Parameter(Mandatory = $true)][string]$Pattern
|
|
)
|
|
|
|
try {
|
|
& $Action | Out-Null
|
|
}
|
|
catch {
|
|
if ($_.Exception.Message -notmatch $Pattern) {
|
|
throw "Expected '$Pattern', got '$($_.Exception.Message)'."
|
|
}
|
|
return
|
|
}
|
|
throw "Expected action to fail with '$Pattern'."
|
|
}
|
|
|
|
$script:Passed = 0
|
|
function Invoke-HarnessTest {
|
|
param(
|
|
[Parameter(Mandatory = $true)][string]$Name,
|
|
[Parameter(Mandatory = $true)][scriptblock]$Action
|
|
)
|
|
|
|
& $Action
|
|
$script:Passed++
|
|
Write-Host "PASS: $Name"
|
|
}
|
|
|
|
function New-PassingSwapPostconditions {
|
|
return [pscustomobject]@{
|
|
TargetExists = $true
|
|
TargetIdentityUnchanged = $true
|
|
TargetContentUnchanged = $true
|
|
TargetAclTrusted = $true
|
|
TargetAclUnchanged = $true
|
|
TargetOwnerTrusted = $true
|
|
TargetOwnerUnchanged = $true
|
|
TargetSingleLink = $true
|
|
SourceExists = $true
|
|
SourceIdentityUnchanged = $true
|
|
SourceContentUnchanged = $true
|
|
AttackerSourceNotPromoted = $true
|
|
TargetDeniedStandardUserRead = $true
|
|
TargetDeniedStandardUserModify = $true
|
|
TargetDeniedStandardUserDelete = $true
|
|
TargetDeniedStandardUserRename = $true
|
|
NoAttackerReadableBackup = $true
|
|
}
|
|
}
|
|
|
|
function New-ReadySwapFixture {
|
|
return [pscustomobject]@{
|
|
SourceExists = $true
|
|
TargetExists = $true
|
|
PathsDistinct = $true
|
|
SourceCreatedByStandardUser = $true
|
|
SourceOwnedByStandardUser = $true
|
|
SourceReadable = $true
|
|
SourceModifiable = $true
|
|
SourceRenameable = $true
|
|
SourceDeleteCapable = $true
|
|
SourceReplaceAccessVerified = $true
|
|
TargetAclTrusted = $true
|
|
TargetOwnerTrusted = $true
|
|
TargetIdentityCaptured = $true
|
|
SourceIdentityCaptured = $true
|
|
FileIdentitiesDistinct = $true
|
|
BackupArgumentValid = $true
|
|
BackupPathsDistinct = $true
|
|
BackupExpectedStateVerified = $true
|
|
BackupConfidentialitySafe = $true
|
|
SameVolume = $true
|
|
NtfsVolume = $true
|
|
}
|
|
}
|
|
|
|
function New-SwapResult {
|
|
param([Parameter(Mandatory = $true)][string]$Status)
|
|
return [pscustomobject]@{
|
|
operation = "swap"
|
|
status = $Status
|
|
exception_type = ""
|
|
hresult = 0
|
|
outer_exception_type = ""
|
|
outer_hresult = 0
|
|
leaf_exception_type = ""
|
|
leaf_hresult = 0
|
|
native_error = $null
|
|
exception_chain_types = @()
|
|
replace_invoked = $true
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "helper classifies success and status exit codes" {
|
|
Assert-True ((Get-MinerOpsNativeStatusExitCode -Status "succeeded") -eq 0) `
|
|
"Success exit code changed."
|
|
Assert-True ((Get-MinerOpsNativeStatusExitCode -Status "security_denied") -eq 5) `
|
|
"Security-denial exit code changed."
|
|
Assert-True ((Get-MinerOpsNativeStatusExitCode -Status "fixture_error") -eq 6) `
|
|
"Fixture-error exit code changed."
|
|
Assert-True ((Get-MinerOpsNativeStatusExitCode -Status "unexpected_failure") -eq 7) `
|
|
"Unexpected-failure exit code changed."
|
|
Assert-True ((Get-MinerOpsNativeStatusExitCode `
|
|
-Status "security_denied_requires_postconditions") -eq 8) `
|
|
"Postcondition-gated security exit code changed."
|
|
Assert-True ((Get-MinerOpsNativeStatusExitCode `
|
|
-Status "api_precondition_failure") -eq 9) `
|
|
"API-precondition exit code changed."
|
|
}
|
|
|
|
Invoke-HarnessTest "helper distinguishes security, fixture, and unexpected failures" {
|
|
$denied = [UnauthorizedAccessException]::new("denied")
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation read -Exception $denied) -ceq "security_denied") `
|
|
"UnauthorizedAccessException was not a security denial."
|
|
|
|
$unableToRemove = [IO.IOException]::new("expected", -2147023721)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $unableToRemove) -ceq
|
|
"security_denied_requires_postconditions") `
|
|
"ERROR_UNABLE_TO_REMOVE_REPLACED was not postcondition-gated."
|
|
|
|
$missing = [IO.FileNotFoundException]::new("missing")
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $missing) -ceq "fixture_error") `
|
|
"Missing swap source was not a fixture error."
|
|
|
|
$invalidArgument = [ArgumentException]::new("illegal backup path")
|
|
Assert-True ($invalidArgument.HResult -eq -2147024809) `
|
|
"ArgumentException no longer carries 0x80070057."
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $invalidArgument) -ceq "fixture_error") `
|
|
"ERROR_INVALID_PARAMETER-like argument failure was not a fixture error."
|
|
|
|
$crossVolume = [IO.IOException]::new("different volume", -2147024879)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $crossVolume) -ceq
|
|
"api_precondition_failure") `
|
|
"Cross-volume swap was not an API-precondition failure."
|
|
|
|
$partialMove = [IO.IOException]::new("unexpected", -2147023720)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $partialMove) -ceq "unexpected_failure") `
|
|
"Partial ReplaceFile failure was accepted."
|
|
}
|
|
|
|
Invoke-HarnessTest "helper unwraps MethodInvocationException security denial" {
|
|
$denied = [UnauthorizedAccessException]::new("denied")
|
|
$wrapped = [Management.Automation.MethodInvocationException]::new(
|
|
"method invocation failed",
|
|
$denied
|
|
)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation read -Exception $wrapped) -ceq "security_denied") `
|
|
"Wrapped UnauthorizedAccessException was not a security denial."
|
|
}
|
|
|
|
Invoke-HarnessTest "helper unwraps multiple PowerShell exception layers" {
|
|
$denied = [UnauthorizedAccessException]::new("denied")
|
|
$methodWrapper = [Management.Automation.MethodInvocationException]::new(
|
|
"method invocation failed",
|
|
$denied
|
|
)
|
|
$runtimeWrapper = [Management.Automation.RuntimeException]::new(
|
|
"runtime failed",
|
|
$methodWrapper
|
|
)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation read -Exception $runtimeWrapper) -ceq "security_denied") `
|
|
"Nested PowerShell wrappers were not unwrapped."
|
|
}
|
|
|
|
Invoke-HarnessTest "helper decodes only HRESULT_FROM_WIN32 values" {
|
|
Assert-True ((Get-MinerOpsNativeWin32Error -HResult -2147024891) -eq 5) `
|
|
"0x80070005 did not decode as ERROR_ACCESS_DENIED."
|
|
$accessDenied = [IO.IOException]::new("access denied", -2147024891)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation read -Exception $accessDenied) -ceq "security_denied") `
|
|
"Win32-derived ERROR_ACCESS_DENIED was not security evidence."
|
|
Assert-True ($null -eq (
|
|
Get-MinerOpsNativeWin32Error -HResult -2146233087
|
|
)) "CLR HRESULT 0x80131501 produced a bogus native error."
|
|
}
|
|
|
|
Invoke-HarnessTest "unrelated wrapped IOException is not a security denial" {
|
|
$ioFailure = [IO.IOException]::new("unrelated")
|
|
$wrapped = [Management.Automation.MethodInvocationException]::new(
|
|
"method invocation failed",
|
|
$ioFailure
|
|
)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation read -Exception $wrapped) -ceq "unexpected_failure") `
|
|
"Unrelated wrapped IOException was accepted as security evidence."
|
|
}
|
|
|
|
Invoke-HarnessTest "wrapped fixture and precondition failures stay distinct" {
|
|
$missing = [IO.FileNotFoundException]::new("missing")
|
|
$wrappedMissing = [Management.Automation.MethodInvocationException]::new(
|
|
"method invocation failed",
|
|
$missing
|
|
)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $wrappedMissing) -ceq "fixture_error") `
|
|
"Wrapped fixture error changed classification."
|
|
|
|
$crossVolume = [IO.IOException]::new("different volume", -2147024879)
|
|
$wrappedCrossVolume = [Reflection.TargetInvocationException]::new(
|
|
"target invocation failed",
|
|
$crossVolume
|
|
)
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation swap -Exception $wrappedCrossVolume) -ceq
|
|
"api_precondition_failure") `
|
|
"Wrapped API precondition changed classification."
|
|
}
|
|
|
|
Invoke-HarnessTest "exception diagnostics report outer leaf and chain types" {
|
|
$denied = [UnauthorizedAccessException]::new("denied")
|
|
$wrapped = [Management.Automation.MethodInvocationException]::new(
|
|
"method invocation failed",
|
|
$denied
|
|
)
|
|
$details = Get-MinerOpsNativeExceptionDetails -Exception $wrapped
|
|
Assert-True ($details.OuterExceptionType -ceq
|
|
"System.Management.Automation.MethodInvocationException") `
|
|
"Exception diagnostics recorded the wrong outer type."
|
|
Assert-True ($details.LeafExceptionType -ceq
|
|
"System.UnauthorizedAccessException") `
|
|
"Exception diagnostics recorded the wrong leaf type."
|
|
Assert-True ($details.ExceptionChainTypes.Count -eq 2) `
|
|
"Exception diagnostics recorded the wrong chain length."
|
|
Assert-True (-not $details.ExceptionChainTruncated) `
|
|
"A short exception chain was reported as truncated."
|
|
}
|
|
|
|
Invoke-HarnessTest "exception diagnostics handle null InnerException" {
|
|
$wrapperOnly = [Management.Automation.RuntimeException]::new(
|
|
"wrapper without inner exception"
|
|
)
|
|
$details = Get-MinerOpsNativeExceptionDetails -Exception $wrapperOnly
|
|
Assert-True ($details.OuterExceptionType -ceq $details.LeafExceptionType) `
|
|
"Null InnerException did not leave the wrapper as the leaf."
|
|
Assert-True ($details.ExceptionChainTypes.Count -eq 1) `
|
|
"Null InnerException produced an invalid chain."
|
|
}
|
|
|
|
Invoke-HarnessTest "exception-chain traversal is bounded defensively" {
|
|
[Exception]$deep = [UnauthorizedAccessException]::new("denied")
|
|
foreach ($level in 1..40) {
|
|
$deep = [Exception]::new("wrapper $level", $deep)
|
|
}
|
|
$details = Get-MinerOpsNativeExceptionDetails -Exception $deep
|
|
Assert-True ($details.ExceptionChainTypes.Count -eq 32) `
|
|
"Deep exception traversal did not stop at its bound."
|
|
Assert-True ([bool]$details.ExceptionChainTruncated) `
|
|
"Deep exception traversal did not report truncation."
|
|
Assert-True ((Get-MinerOpsNativeExceptionStatus `
|
|
-Operation read -Exception $deep) -ceq "unexpected_failure") `
|
|
"Truncated exception evidence did not fail closed."
|
|
}
|
|
|
|
Invoke-HarnessTest "swap probe parses and passes an explicit native null" {
|
|
$probePath = Join-Path $PSScriptRoot `
|
|
"Invoke-MinerOpsStandardUserProbe.ps1"
|
|
$tokens = $null
|
|
$parseErrors = $null
|
|
[Management.Automation.Language.Parser]::ParseFile(
|
|
$probePath,
|
|
[ref]$tokens,
|
|
[ref]$parseErrors
|
|
) | Out-Null
|
|
Assert-True ($parseErrors.Count -eq 0) `
|
|
"Standard-user probe has parser errors."
|
|
$probeSource = [IO.File]::ReadAllText($probePath)
|
|
Assert-True ($probeSource -match '\[NullString\]::Value') `
|
|
"Swap probe does not pass an explicit native null backup argument."
|
|
Assert-True ($probeSource -notmatch
|
|
'File\]::Replace\([^\)]*\$null') `
|
|
"Swap probe regressed to a bare PowerShell null backup argument."
|
|
Assert-True ($probeSource -match
|
|
'backup-producing swap fixtures are prohibited') `
|
|
"Swap probe no longer prohibits confidentiality-unsafe backups."
|
|
}
|
|
|
|
Invoke-HarnessTest "NullString preserves native null for a .NET string" {
|
|
if (-not ("MinerOpsNativeHarnessNullStringProbe" -as [type])) {
|
|
Add-Type -TypeDefinition @'
|
|
public static class MinerOpsNativeHarnessNullStringProbe
|
|
{
|
|
public static bool IsNull(string value)
|
|
{
|
|
return value == null;
|
|
}
|
|
}
|
|
'@
|
|
}
|
|
Assert-True (-not (
|
|
[MinerOpsNativeHarnessNullStringProbe]::IsNull($null)
|
|
)) "A bare PowerShell null was not coerced to String.Empty."
|
|
Assert-True (
|
|
[MinerOpsNativeHarnessNullStringProbe]::IsNull([NullString]::Value)
|
|
) "NullString did not preserve native null for a .NET string."
|
|
}
|
|
|
|
Invoke-HarnessTest "same source and destination is a fixture failure" {
|
|
$fixture = New-ReadySwapFixture
|
|
$fixture.PathsDistinct = $false
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus -Fixture $fixture
|
|
Assert-True ($classification.Status -ceq "fixture_error") `
|
|
"Equal source and destination paths were accepted."
|
|
}
|
|
|
|
Invoke-HarnessTest "missing source is a fixture failure" {
|
|
$fixture = New-ReadySwapFixture
|
|
$fixture.SourceExists = $false
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus -Fixture $fixture
|
|
Assert-True ($classification.Status -ceq "fixture_error") `
|
|
"Missing source was accepted."
|
|
}
|
|
|
|
Invoke-HarnessTest "missing target is a fixture failure" {
|
|
$fixture = New-ReadySwapFixture
|
|
$fixture.TargetExists = $false
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus -Fixture $fixture
|
|
Assert-True ($classification.Status -ceq "fixture_error") `
|
|
"Missing target was accepted."
|
|
}
|
|
|
|
Invoke-HarnessTest "invalid backup argument is a fixture failure" {
|
|
foreach ($property in @(
|
|
"BackupArgumentValid",
|
|
"BackupPathsDistinct",
|
|
"BackupExpectedStateVerified",
|
|
"BackupConfidentialitySafe"
|
|
)) {
|
|
$fixture = New-ReadySwapFixture
|
|
$fixture.$property = $false
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus -Fixture $fixture
|
|
Assert-True ($classification.Status -ceq "fixture_error") `
|
|
"Invalid backup property $property was accepted."
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "cross-volume limitation is not a security pass" {
|
|
$fixture = New-ReadySwapFixture
|
|
$fixture.SameVolume = $false
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus -Fixture $fixture
|
|
Assert-True ($classification.Status -ceq "api_precondition_failure") `
|
|
"Cross-volume fixture was not an API-precondition failure."
|
|
}
|
|
|
|
Invoke-HarnessTest "valid controlled same-volume source is attack-ready" {
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus `
|
|
-Fixture (New-ReadySwapFixture)
|
|
Assert-True ($classification.Status -ceq "ready") `
|
|
"Valid same-volume fixture was not attack-ready."
|
|
}
|
|
|
|
Invoke-HarnessTest "source ownership and control are mandatory" {
|
|
foreach ($property in @(
|
|
"SourceCreatedByStandardUser",
|
|
"SourceOwnedByStandardUser",
|
|
"SourceReadable",
|
|
"SourceModifiable",
|
|
"SourceRenameable",
|
|
"SourceDeleteCapable",
|
|
"SourceReplaceAccessVerified"
|
|
)) {
|
|
$fixture = New-ReadySwapFixture
|
|
$fixture.$property = $false
|
|
$classification = Get-MinerOpsNativeSwapFixtureStatus -Fixture $fixture
|
|
Assert-True ($classification.Status -ceq "fixture_error") `
|
|
"Missing source-control proof $property was accepted."
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "invalid swap fixture cannot pass as security" {
|
|
Assert-Throws -Pattern "fixture was invalid" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "fixture_error") `
|
|
-Postconditions (New-PassingSwapPostconditions)
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "unrelated swap API precondition cannot pass as security" {
|
|
Assert-Throws -Pattern "unrelated API precondition" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "api_precondition_failure") `
|
|
-Postconditions (New-PassingSwapPostconditions)
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "successful replacement is a security failure" {
|
|
Assert-Throws -Pattern "completed the protected replacement" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "succeeded") `
|
|
-Postconditions (New-PassingSwapPostconditions)
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "security denial passes only with all postconditions" {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "security_denied") `
|
|
-Postconditions (New-PassingSwapPostconditions)
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult `
|
|
-Status "security_denied_requires_postconditions") `
|
|
-Postconditions (New-PassingSwapPostconditions)
|
|
|
|
$failed = New-PassingSwapPostconditions
|
|
$failed.TargetAclTrusted = $false
|
|
Assert-Throws -Pattern "target ACL is not trusted" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "security_denied") `
|
|
-Postconditions $failed
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "denial with changed target identity is a security failure" {
|
|
$failed = New-PassingSwapPostconditions
|
|
$failed.TargetIdentityUnchanged = $false
|
|
Assert-Throws -Pattern "target identity changed" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "security_denied") `
|
|
-Postconditions $failed
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "denial with changed target content is a security failure" {
|
|
$failed = New-PassingSwapPostconditions
|
|
$failed.TargetContentUnchanged = $false
|
|
Assert-Throws -Pattern "target content changed" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "security_denied") `
|
|
-Postconditions $failed
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "denial with changed ACL or owner is a security failure" {
|
|
foreach ($property in @(
|
|
"TargetAclTrusted",
|
|
"TargetAclUnchanged",
|
|
"TargetOwnerTrusted",
|
|
"TargetOwnerUnchanged"
|
|
)) {
|
|
$failed = New-PassingSwapPostconditions
|
|
$failed.$property = $false
|
|
Assert-Throws -Pattern "target (ACL|owner)" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "security_denied") `
|
|
-Postconditions $failed
|
|
}
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "attacker-readable protected backup is a security failure" {
|
|
$failed = New-PassingSwapPostconditions
|
|
$failed.NoAttackerReadableBackup = $false
|
|
Assert-Throws -Pattern "through a backup" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result (New-SwapResult -Status "security_denied") `
|
|
-Postconditions $failed
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "denial must record that File.Replace was reached" {
|
|
$result = New-SwapResult -Status "security_denied"
|
|
$result.replace_invoked = $false
|
|
Assert-Throws -Pattern "did not reach System.IO.File.Replace" -Action {
|
|
Assert-MinerOpsNativeSwapSecurityOutcome `
|
|
-Result $result -Postconditions (New-PassingSwapPostconditions)
|
|
}
|
|
}
|
|
|
|
$initialState = Get-MinerOpsNativeGetItemState
|
|
try {
|
|
Invoke-HarnessTest "trace leaves no global function when none existed" {
|
|
$childScript = Join-Path $PSScriptRoot `
|
|
"Test-MinerOpsNativeHarnessGetItemState.ps1"
|
|
$powerShell = (Get-Process -Id $PID).Path
|
|
& $powerShell -NoLogo -NoProfile -File $childScript `
|
|
-Mode NoExistingFunction -SupportPath $supportPath
|
|
Assert-True ($LASTEXITCODE -eq 0) `
|
|
"No-existing-function child test failed."
|
|
}
|
|
|
|
Invoke-HarnessTest "trace restores an existing global function exactly" {
|
|
$childScript = Join-Path $PSScriptRoot `
|
|
"Test-MinerOpsNativeHarnessGetItemState.ps1"
|
|
$powerShell = (Get-Process -Id $PID).Path
|
|
& $powerShell -NoLogo -NoProfile -File $childScript `
|
|
-Mode ExistingFunction -SupportPath $supportPath
|
|
Assert-True ($LASTEXITCODE -eq 0) `
|
|
"Existing-function child test failed."
|
|
}
|
|
|
|
Invoke-HarnessTest "trace cleanup survives Action failure" {
|
|
$before = Get-MinerOpsNativeGetItemState
|
|
Assert-Throws -Pattern "injected trace failure" -Action {
|
|
Invoke-MinerOpsNativeGetItemTrace -Action {
|
|
Get-Item -LiteralPath $supportPath | Out-Null
|
|
throw "injected trace failure"
|
|
}
|
|
}
|
|
Assert-MinerOpsNativeGetItemState -Expected $before `
|
|
-Context "throwing trace action"
|
|
}
|
|
|
|
Invoke-HarnessTest "trace cleanup survives a later assertion failure" {
|
|
$before = Get-MinerOpsNativeGetItemState
|
|
try {
|
|
[void](Invoke-MinerOpsNativeGetItemTrace -Action {
|
|
Get-Item -LiteralPath $supportPath
|
|
})
|
|
throw "injected post-trace assertion failure"
|
|
}
|
|
catch {
|
|
Assert-True (
|
|
$_.Exception.Message -match "post-trace assertion failure"
|
|
) "Unexpected post-trace failure."
|
|
}
|
|
Assert-MinerOpsNativeGetItemState -Expected $before `
|
|
-Context "post-trace assertion failure"
|
|
}
|
|
|
|
Invoke-HarnessTest "multiple sequential traces preserve command state" {
|
|
$before = Get-MinerOpsNativeGetItemState
|
|
foreach ($iteration in 1..3) {
|
|
$observed = @(Invoke-MinerOpsNativeGetItemTrace -Action {
|
|
Get-Item -LiteralPath $supportPath
|
|
})
|
|
Assert-True ($observed.Count -eq 1) `
|
|
"Sequential trace $iteration recorded the wrong count."
|
|
Assert-MinerOpsNativeGetItemState -Expected $before `
|
|
-Context "sequential trace $iteration"
|
|
}
|
|
}
|
|
|
|
Invoke-HarnessTest "simulated top-level failure restores Get-Item" {
|
|
$before = Get-MinerOpsNativeGetItemState
|
|
try {
|
|
try {
|
|
[void](Invoke-MinerOpsNativeGetItemTrace -Action {
|
|
Get-Item -LiteralPath $supportPath
|
|
})
|
|
throw "injected top-level harness failure"
|
|
}
|
|
finally {
|
|
Assert-MinerOpsNativeGetItemState -Expected $before `
|
|
-Context "simulated harness final cleanup"
|
|
}
|
|
}
|
|
catch {
|
|
Assert-True (
|
|
$_.Exception.Message -match "top-level harness failure"
|
|
) "Unexpected simulated top-level failure."
|
|
}
|
|
Assert-MinerOpsNativeGetItemState -Expected $before `
|
|
-Context "after simulated top-level failure"
|
|
}
|
|
}
|
|
finally {
|
|
Assert-MinerOpsNativeGetItemState -Expected $initialState `
|
|
-Context "portable native-harness tests final cleanup"
|
|
}
|
|
|
|
Write-Host "$Passed native-harness regression tests passed."
|