Browse Source

test: exercise item, link and descriptor failure paths

Cover locked and denied copy and move, recursive enumeration that stops or meets a broken junction, hard link counts on a network share, denied link creation, the default root folder of a drive root, ownerless -PassThru and an undefined hash algorithm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Co-authored-by: AI Assistant <ai@example.com>
pull/118/head
Raimund Andree 2 days ago
parent
commit
360417a5c3
  1. 13
      Tests/Access.Tests.ps1
  2. 11
      Tests/FileHash.Tests.ps1
  3. 126
      Tests/ItemCmdlets.Tests.ps1
  4. 53
      Tests/Links.Tests.ps1
  5. 14
      Tests/Owner.Tests.ps1

13
Tests/Access.Tests.ps1

@ -388,6 +388,19 @@ Describe 'Get-NTFSSimpleAccess' {
@($result | Where-Object -Property FullName -EQ -Value $root) | Should -HaveCount $rootAlone.Count
}
# With -IncludeRootFolder, the default, the cmdlet reports the parent folder of the first path first. A drive root
# has none, so it reports the root itself, once. The test reads the entries of the drive root only.
It 'Should report no parent folder in front of a drive root by default' {
$root = [IO.Path]::GetPathRoot($child)
$rootAlone = @(Get-NTFSSimpleAccess -Path $root -IncludeRootFolder:$false -ErrorAction Stop)
$result = @(Get-NTFSSimpleAccess -Path $root -ErrorVariable simpleErrors -ErrorAction SilentlyContinue)
$simpleErrors | Should -BeNullOrEmpty
$result | Should -HaveCount $rootAlone.Count
$result | ForEach-Object -Process { $_.FullName | Should -Be $root }
}
# Windows doesn't distinguish paths by case. Before 5.0.0-rc7, the cmdlet didn't recognize the parent folder of a
# folder whose path differed from it in case, and left the folder out.
It 'Should compare a folder with its parent folder also when their paths differ in case' {

11
Tests/FileHash.Tests.ps1

@ -61,6 +61,17 @@ Describe 'Get-FileHash2' {
$hashError.FullyQualifiedErrorId | Should -BeLike 'HashAlgorithmNotAvailable,*'
}
# PowerShell binds only the named algorithms to -Algorithm, so a program that calls the public method with an
# undefined value is the only way to get here.
It 'Should refuse an algorithm that the enumeration does not define when the public method creates it' {
$unknown = [Enum]::ToObject([Security2.FileSystem.FileInfo.HashAlgorithms], 99)
$failure = { [Security2.FileSystem.FileInfo.Extensions]::CreateHashAlgorithm($unknown) } | Should -Throw -PassThru
$failure.Exception.GetBaseException() | Should -BeOfType [System.ArgumentOutOfRangeException]
$failure.Exception.GetBaseException().ParamName | Should -BeExactly 'algorithm'
}
It 'Should warn once that MACTripleDES is deprecated' -Skip:$isCore {
$results = @(Get-FileHash2 -Path $first, $second -Algorithm MACTripleDES -WarningVariable hashWarnings -WarningAction SilentlyContinue)

126
Tests/ItemCmdlets.Tests.ps1

@ -146,6 +146,32 @@ Describe 'Get-ChildItem2' {
$result | Should -HaveCount 1
$childErrors | Should -BeNullOrEmpty
}
# The second file comes from a sub folder, so the pipeline stops while the cmdlet is inside the recursion.
It 'Should stop a recursive pipeline inside a sub folder without recording an enumeration error' {
$result = @(Get-ChildItem2 -Path $tree -Recurse -File -ErrorVariable childErrors -ErrorAction SilentlyContinue | Select-Object -First 2)
$result | Should -HaveCount 2
$childErrors | Should -BeNullOrEmpty
}
# A break or continue in a later pipeline stage passes through the cmdlet as an exception, which it must not
# report as a failed folder.
It 'Should end a recursive enumeration for <Keyword> in a later pipeline stage without recording an enumeration error' -ForEach @(
@{ Keyword = 'break' }
@{ Keyword = 'continue' }
) {
$names = [System.Collections.Generic.List[string]]::new()
foreach ($round in 1) {
Get-ChildItem2 -Path $tree -Recurse -File -ErrorVariable childErrors -ErrorAction SilentlyContinue | ForEach-Object -Process {
$names.Add($_.Name)
if ($Keyword -eq 'break') { break } else { continue }
}
}
$names | Should -HaveCount 1
$childErrors | Should -BeNullOrEmpty
}
}
Context 'Unreadable directories' {
@ -200,6 +226,31 @@ Describe 'Get-ChildItem2' {
$result[0].FullName | Should -Be $link
Get-Content -LiteralPath $file | Should -Be 'Target'
}
# A junction whose target is gone passes the existence check, but the folder behind it can't be opened. The
# error belongs to that folder, and the enumeration goes on with the next one.
It 'Should report a junction whose target was removed as a DirUnspecifiedError and continue with the next folder' {
$root = New-TestSandboxItem -Sandbox $sandbox -Name 'BrokenJunction' -Directory
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'RemovedTarget' -Directory
$link = Join-Path -Path $root -ChildPath 'Broken'
$sibling = Join-Path -Path $root -ChildPath 'Sibling'
$file = Join-Path -Path $sibling -ChildPath 'Sibling.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link, $sibling, $file
New-Item -ItemType Directory -Path $sibling | Out-Null
Set-Content -LiteralPath $file -Value 'Sibling'
New-Item -ItemType Junction -Path $link -Value $target | Out-Null
Remove-Item -LiteralPath $target -Force
$result = @(Get-ChildItem2 -Path $root -Recurse -ErrorVariable childErrors -ErrorAction SilentlyContinue)
$childErrors | Should -HaveCount 1
$childErrors[0].FullyQualifiedErrorId | Should -BeLike 'DirUnspecifiedError,*'
$childErrors[0].CategoryInfo.Category | Should -Be 'NotSpecified'
$childErrors[0].TargetObject | Should -Be $link
$childErrors[0].Exception | Should -BeOfType [System.IO.DirectoryNotFoundException]
@($result.FullName | Sort-Object) | Should -Be @(@($link, $sibling, $file) | Sort-Object)
Get-Content -LiteralPath $file | Should -Be 'Sibling'
}
}
Context 'Optional object properties' {
@ -244,6 +295,29 @@ Describe 'Get-ChildItem2' {
$item.Mode | Should -BeExactly '--rhs'
}
# Windows can't list the hard links of a file on a network share, (50) "The request is not supported". The cmdlet
# still returns the file, without HardLinkCount, and says why in a debug message. The test sets the preference,
# because -Debug would prompt in Windows PowerShell.
It 'Should return a file on a network share without HardLinkCount and say why in a debug message' -Skip:(-not $canUseAdminShare) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareProperties' -Directory
$file = Join-Path -Path $folder -ChildPath 'Share.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'Share'
$sharePath = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $file
$settings['IdentifyHardLinks'] = $true
$DebugPreference = 'Continue'
$output = @(Get-ChildItem2 -Path $sharePath -ErrorVariable childErrors -ErrorAction SilentlyContinue 5>&1)
$childErrors | Should -BeNullOrEmpty
$items = @($output | Where-Object -FilterScript { $_ -isnot [Management.Automation.DebugRecord] })
$items | Should -HaveCount 1
$items[0].Name | Should -BeExactly 'Share.txt'
$items[0].PSObject.Properties['HardLinkCount'] | Should -BeNullOrEmpty
$messages = @($output | Where-Object -FilterScript { $_ -is [Management.Automation.DebugRecord] } | ForEach-Object -Process { $_.Message })
$messages | Should -Contain "Could not read hard links for '$sharePath'"
}
}
Context 'Default table view' {
@ -557,6 +631,58 @@ Describe 'Copy-Item2, Move-Item2, and Remove-Item2 with several paths' {
$itemErrors[0].Exception.Message | Should -BeLike "*'$missingShare'*"
$first | Should -Exist
}
# A sharing violation is an IOException, which both cmdlets write as InvalidData; the error belongs to its source
# only, and no object comes out for it with -PassThru.
It '<Command> should write a <ErrorId> for a source that another process has locked and continue with the next path' -ForEach @(
@{ Command = 'Copy-Item2'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; ErrorId = 'MoveError' }
) {
$stream = [IO.File]::Open($first, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None)
try {
$result = @(& $Command -Path $first, $second -Destination $destination -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue)
}
finally {
$stream.Dispose()
}
$itemErrors | Should -HaveCount 1
$itemErrors[0].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[0].CategoryInfo.Category | Should -Be 'InvalidData'
$itemErrors[0].TargetObject | Should -Be $first
$itemErrors[0].Exception | Should -BeOfType [System.IO.IOException]
$result | Should -HaveCount 1
$result[0].FullName | Should -Be (Join-Path -Path $destination -ChildPath 'Second.txt')
Join-Path -Path $destination -ChildPath 'First.txt' | Should -Not -Exist
Get-Content -LiteralPath $first | Should -Be 'First'
Get-Content -LiteralPath (Join-Path -Path $destination -ChildPath 'Second.txt') | Should -Be 'Second'
}
# Any other failure of Windows is not an IOException, and both cmdlets write it as NotSpecified. A deny entry for
# Everyone also applies to an administrator, who doesn't bypass the DACL without a backup privilege.
It '<Command> should write a <ErrorId> for each source when the destination folder denies new files' -ForEach @(
@{ Command = 'Copy-Item2'; ErrorId = 'CopyError' }
@{ Command = 'Move-Item2'; ErrorId = 'MoveError' }
) {
$denied = Join-Path -Path $folder -ChildPath 'Denied'
Assert-TestSandboxPath -Sandbox $sandbox -Path $denied
New-Item -ItemType Directory -Path $denied | Out-Null
Add-TestDenyRule -Sandbox $sandbox -Path $denied -Rights @{ 'S-1-1-0' = 'CreateFiles' }
$result = @(& $Command -Path $first, $second -Destination $denied -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue)
$itemErrors | Should -HaveCount 2
for ($index = 0; $index -lt 2; $index++) {
$itemErrors[$index].FullyQualifiedErrorId | Should -BeLike "$ErrorId,*"
$itemErrors[$index].CategoryInfo.Category | Should -Be 'NotSpecified'
$itemErrors[$index].TargetObject | Should -Be @($first, $second)[$index]
$itemErrors[$index].Exception | Should -BeOfType [System.UnauthorizedAccessException]
}
$result | Should -BeNullOrEmpty
@(Get-ChildItem -LiteralPath $denied -Force) | Should -BeNullOrEmpty
Get-Content -LiteralPath $first | Should -Be 'First'
Get-Content -LiteralPath $second | Should -Be 'Second'
}
}
Describe 'Move-Item2' {

53
Tests/Links.Tests.ps1

@ -159,6 +159,25 @@ Describe 'New-NTFSHardLink' {
$linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*'
$result | Should -BeNullOrEmpty
}
It 'Should write a PermissionDenied error and create no link in a folder that denies new files' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'DeniedTarget'
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'DeniedFolder' -Directory
$link = Join-Path -Path $folder -ChildPath 'Link.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
Add-TestDenyRule -Sandbox $sandbox -Path $folder -Rights @{ 'S-1-1-0' = 'CreateFiles' }
$result = @(New-NTFSHardLink -Path $link -Target $target -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue)
$linkErrors | Should -HaveCount 1
$linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'CreateHardLinkError,*'
$linkErrors[0].CategoryInfo.Category | Should -Be 'PermissionDenied'
$linkErrors[0].TargetObject | Should -Be $link
$linkErrors[0].Exception | Should -BeOfType [System.UnauthorizedAccessException]
$result | Should -BeNullOrEmpty
$link | Should -Not -Exist
Get-Content -LiteralPath $target | Should -Be 'DeniedTarget'
}
}
Describe 'Get-NTFSHardLink' {
@ -368,6 +387,26 @@ Describe 'New-NTFSSymbolicLink' {
'0x{0:X8}' -f $linkErrors[0].Exception.HResult | Should -Be '0x80070522'
Test-Path2 -Path $link | Should -BeFalse
}
# With the right to create symbolic links, Windows refuses the link only for the folder of the link, which denies
# new files here.
It 'Should write a PermissionDenied error and create no link in a folder that denies new files' -Skip:(-not $canCreateSymbolicLinks) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicDeniedTarget'
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicDeniedFolder' -Directory
$link = Join-Path -Path $folder -ChildPath 'Link.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
Add-TestDenyRule -Sandbox $sandbox -Path $folder -Rights @{ 'S-1-1-0' = 'CreateFiles' }
$result = @(New-NTFSSymbolicLink -Path $link -Target $target -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue)
$linkErrors | Should -HaveCount 1
$linkErrors[0].FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*'
$linkErrors[0].CategoryInfo.Category | Should -Be 'PermissionDenied'
$linkErrors[0].TargetObject | Should -Be $link
$linkErrors[0].Exception | Should -BeOfType [System.UnauthorizedAccessException]
$result | Should -BeNullOrEmpty
Test-Path2 -Path $link | Should -BeFalse
}
}
# Each error names its item, so that the errors of many links can be told apart. Before 5.0.0-rc7, the errors of
@ -456,4 +495,18 @@ Describe 'Parameters of the cmdlets that create links' {
$sets | Should -Not -BeNullOrEmpty
$sets | ForEach-Object -Process { $_.IsMandatory | Should -BeTrue }
}
# PowerShell reads a parameter that takes pipeline input before it binds the input, so the getter must not fail
# while the cmdlet has no -Path. Before 5.0.0-rc7, it threw an index error, and every piped object failed with
# GetDefaultValueFailed.
It '<Type> should return no -Path until it has one, and the first one afterwards' -ForEach @(
@{ Type = 'NTFSSecurity.NewHardLink' }
@{ Type = 'NTFSSecurity.NewSymbolicLink' }
) {
$cmdlet = New-Object -TypeName $Type
$cmdlet.Path | Should -BeNullOrEmpty
$cmdlet.Path = 'C:\NTFSSecurity\Link.txt'
$cmdlet.Path | Should -BeExactly 'C:\NTFSSecurity\Link.txt'
}
}

14
Tests/Owner.Tests.ps1

@ -299,5 +299,19 @@ Describe 'Set-NTFSOwner' {
Set-NTFSSecurityDescriptor -SecurityDescriptor $sd -ErrorAction Stop
Get-TestOwner -Path $file | Should -Be $currentUser
}
It 'Should write nothing without -PassThru and leave the owner of the item unchanged' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'SetOwnerDescriptorQuiet'
$owner = Get-TestOwner -Path $file
$sd = Get-NTFSSecurityDescriptor -Path $file
$sidType = [System.Security.Principal.SecurityIdentifier]
$sd.SecurityDescriptor.GetOwner($sidType).Value | Should -Not -Be 'S-1-1-0'
$result = @(Set-NTFSOwner -SecurityDescriptor $sd -Account 'S-1-1-0' -ErrorAction Stop)
$result | Should -BeNullOrEmpty
$sd.SecurityDescriptor.GetOwner($sidType).Value | Should -Be 'S-1-1-0'
Get-TestOwner -Path $file | Should -Be $owner
}
}
}

Loading…
Cancel
Save