@ -14,6 +14,10 @@ param ()
BeforeDiscovery {
BeforeDiscovery {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
$canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege'
$canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege'
# The restricted token of the basic-user runner cannot map a drive letter or reach the administrative share.
$canMapDrive = Test-DriveMappingAvailable
$canUseAdminShare = Test-AdminShareAvailable
$isElevated = Test-IsElevated
}
}
BeforeAll {
BeforeAll {
@ -593,3 +597,792 @@ Describe 'Get-FileHash2 with pipeline input and ordered failures' {
$relativeResult . Hash | Should -BeExactly $result [ 0 ] . Hash
$relativeResult . Hash | Should -BeExactly $result [ 0 ] . Hash
}
}
}
}
Describe 'Bracket and wildcard characters in a path are treated literally' {
# A bracket is an ordinary character in a path, as the existing -Filter test already pins for Get-ChildItem2
# (ItemCmdlets.Tests.ps1:202); this closes the same gap for the path parameter of the other item cmdlets.
It '<Command> should treat a path with brackets in the name as a literal file name' -ForEach @ (
@ { Command = 'Copy-Item2' }
@ { Command = 'Move-Item2' }
@ { Command = 'Get-Item2' }
@ { Command = 'Remove-Item2' }
) {
$sourceFolder = New-TestSandboxItem -Sandbox $sandbox -Name ( 'Bracket{0}Source' -f $Command . Replace ( '-' , '' ) ) -Directory
$bracketFile = Join-Path -Path $sourceFolder -ChildPath 'Report[1].txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $bracketFile
Set-Content -LiteralPath $bracketFile -Value 'Bracket'
switch ( $Command ) {
'Copy-Item2' {
$destination = Join-Path -Path $sourceFolder -ChildPath 'Copied'
New-Item -ItemType Directory -Path $destination | Out-Null
$result = Copy-Item2 -Path $bracketFile -Destination $destination -PassThru $true -ErrorAction Stop
$result . Name | Should -Be 'Report[1].txt'
Get-Content -LiteralPath ( Join-Path -Path $destination -ChildPath 'Report[1].txt' ) | Should -Be 'Bracket'
# Should -Exist uses a non-literal Test-Path, which reads the brackets as a character class and
# would misreport a file that is actually there; [IO.File]::Exists is a literal, independent oracle.
[IO.File] :: Exists ( $bracketFile ) | Should -BeTrue
}
'Move-Item2' {
$destination = Join-Path -Path $sourceFolder -ChildPath 'Moved'
New-Item -ItemType Directory -Path $destination | Out-Null
$result = Move-Item2 -Path $bracketFile -Destination $destination -PassThru $true -ErrorAction Stop
$result . Name | Should -Be 'Report[1].txt'
[IO.File] :: Exists ( $bracketFile ) | Should -BeFalse
Get-Content -LiteralPath ( Join-Path -Path $destination -ChildPath 'Report[1].txt' ) | Should -Be 'Bracket'
}
'Get-Item2' {
$result = Get-Item2 -Path $bracketFile -ErrorAction Stop
$result . Name | Should -Be 'Report[1].txt'
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
}
'Remove-Item2' {
$result = Remove-Item2 -Path $bracketFile -PassThru -ErrorAction Stop
$result . Name | Should -Be 'Report[1].txt'
[IO.File] :: Exists ( $bracketFile ) | Should -BeFalse
}
}
}
# '*' and '?' are not in [IO.Path]::GetInvalidPathChars() in either edition (only '|' and the control characters
# are, and Windows PowerShell additionally rejects '"', '<', '>'), so a literal asterisk or question mark reaches
# the file system as an ordinary character instead of raising ArgumentException: AlphaFS, like System.IO, looks
# for the exact literal name and reports it missing rather than expanding the character as a search pattern.
It '<Command> should not expand an asterisk in a path as a wildcard and should report FileNotFound' -ForEach @ (
@ { Command = 'Move-Item2' ; ErrorId = 'FileNotFound' }
@ { Command = 'Remove-Item2' ; ErrorId = 'FileNotFound' }
) {
$sourceFolder = New-TestSandboxItem -Sandbox $sandbox -Name ( 'Wildcard{0}Source' -f $Command . Replace ( '-' , '' ) ) -Directory
$fileA = Join-Path -Path $sourceFolder -ChildPath 'FileA.txt'
$fileB = Join-Path -Path $sourceFolder -ChildPath 'FileB.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $fileA , $fileB
Set-Content -LiteralPath $fileA -Value 'A'
Set-Content -LiteralPath $fileB -Value 'B'
$wildcardPath = Join-Path -Path $sourceFolder -ChildPath 'File*.txt'
switch ( $Command ) {
'Move-Item2' {
$destination = Join-Path -Path $sourceFolder -ChildPath 'Destination'
New-Item -ItemType Directory -Path $destination | Out-Null
Move-Item2 -Path $wildcardPath -Destination $destination -ErrorVariable itemErrors -ErrorAction SilentlyContinue
}
'Remove-Item2' {
Remove-Item2 -Path $wildcardPath -ErrorVariable itemErrors -ErrorAction SilentlyContinue
}
}
$itemErrors | Should -HaveCount 1
$itemErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike " $ ErrorId,* "
$itemErrors [ 0 ] . CategoryInfo . Category | Should -Be 'ObjectNotFound'
$itemErrors [ 0 ] . TargetObject | Should -Be $wildcardPath
$fileA | Should -Exist
$fileB | Should -Exist
Get-Content -LiteralPath $fileA | Should -Be 'A'
Get-Content -LiteralPath $fileB | Should -Be 'B'
}
}
Describe 'Items whose path is longer than 260 characters' {
BeforeAll {
Add-Type -Path ( Join-Path -Path ( Get-Module -Name NTFSSecurity ) . ModuleBase -ChildPath 'AlphaFS.dll' )
# Builds a fresh folder tree and file below it whose full path is longer than 260 characters, in its own
# branch of the sandbox, and returns the file's full path. Mirrors ItemCmdlets.Tests.ps1:1029: only the short
# root goes through Assert-TestSandboxPath, because [IO.Path] (which it calls) throws PathTooLongException
# for the long path itself in Windows PowerShell; AlphaFS does not have that limit. A script block, not a
# function: PowerShell does not parse an attribute on a function statement nested inside another block, so
# PSUseShouldProcessForStateChangingFunctions (its name starts with New) could not be suppressed that way.
$newTestLongPathFile = {
param ( [ Parameter ( Mandatory ) ] [string] $Name )
$root = New-TestSandboxItem -Sandbox $sandbox -Name $Name -Directory
$deep = Join-Path -Path $root -ChildPath ( ( ( 'A' * 100 ) , ( 'B' * 100 ) , ( 'C' * 100 ) ) -join '\' )
[Alphaleonis.Win32.Filesystem.Directory] :: CreateDirectory ( $deep ) | Out-Null
$file = Join-Path -Path $deep -ChildPath 'Long.txt'
[Alphaleonis.Win32.Filesystem.File] :: WriteAllText ( $file , 'LongPathContent' )
if ( $file . Length -le 260 ) {
throw " The test path ' $ file' is not longer than 260 characters. "
}
$file
}
}
It 'Get-Item2 should return the file and its folder' {
$file = & $newTestLongPathFile -Name 'GetItemLong'
$fileResult = Get-Item2 -Path $file -ErrorAction Stop
$folderResult = Get-Item2 -Path ( Split-Path -Path $file -Parent ) -ErrorAction Stop
$fileResult | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$fileResult . FullName | Should -Be $file
$folderResult | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
}
It 'Get-ChildItem2 should list the content of the folder' {
$file = & $newTestLongPathFile -Name 'ChildItemLong'
$result = @ ( Get-ChildItem2 -Path ( Split-Path -Path $file -Parent ) -ErrorAction Stop )
$result | Should -HaveCount 1
$result [ 0 ] . FullName | Should -Be $file
}
It 'Get-FileHash2 should hash the file and match an independent SHA256 read through AlphaFS' {
$file = & $newTestLongPathFile -Name 'HashLong'
$bytes = [Alphaleonis.Win32.Filesystem.File] :: ReadAllBytes ( $file )
$expectedHash = -join ( ( [Security.Cryptography.SHA256] :: Create ( ) . ComputeHash ( $bytes ) ) | ForEach-Object -Process { $_ . ToString ( 'X2' ) } )
$result = Get-FileHash2 -Path $file -ErrorAction Stop
$result . Hash | Should -BeExactly $expectedHash
}
It 'Copy-Item2 should copy the file to a short destination and leave the source in place' {
$file = & $newTestLongPathFile -Name 'CopyLong'
$destination = New-TestSandboxItem -Sandbox $sandbox -Name 'CopyLongDestination' -Directory
$result = Copy-Item2 -Path $file -Destination $destination -PassThru $true -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$copied = Join-Path -Path $destination -ChildPath 'Long.txt'
Get-Content -LiteralPath $copied | Should -Be 'LongPathContent'
[Alphaleonis.Win32.Filesystem.File] :: Exists ( $file ) | Should -BeTrue
}
It 'Move-Item2 should move the file to a short destination and remove the source' {
$file = & $newTestLongPathFile -Name 'MoveLong'
$destination = New-TestSandboxItem -Sandbox $sandbox -Name 'MoveLongDestination' -Directory
$result = Move-Item2 -Path $file -Destination $destination -PassThru $true -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
[Alphaleonis.Win32.Filesystem.File] :: Exists ( $file ) | Should -BeFalse
$moved = Join-Path -Path $destination -ChildPath 'Long.txt'
Get-Content -LiteralPath $moved | Should -Be 'LongPathContent'
}
}
Describe 'Item cmdlets at the root of a mapped drive' -Skip : ( -not $canMapDrive ) {
# NEVER the real root of a volume: a drive letter that subst maps to a sandbox folder is the root of a drive for
# Windows and for the module (see Tests\DriveRoot.Tests.ps1), without touching a real volume.
BeforeAll {
$driveSandbox = New-TestSandbox -Name 'ItemsDriveRoot'
$mapped = New-TestSandboxItem -Sandbox $driveSandbox -Name 'Mapped' -Directory
$driveRoot = New-TestDriveMapping -Sandbox $driveSandbox -Path $mapped
if ( -not $driveRoot ) {
throw 'No drive letter could be mapped to the sandbox folder.'
}
$existingFile = Join-Path -Path $mapped -ChildPath 'Existing.txt'
Assert-TestSandboxPath -Sandbox $driveSandbox -Path $existingFile
Set-Content -LiteralPath $existingFile -Value 'Existing'
}
AfterAll {
if ( $driveRoot ) {
Remove-TestDriveMapping -Root $driveRoot
}
Remove-TestSandbox -Sandbox $driveSandbox
}
It 'Get-Item2 should return a DirectoryInfo object for the root of the mapped drive' {
$result = Get-Item2 -Path $driveRoot -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$result . FullName . TrimEnd ( '\' ) | Should -Be $driveRoot . TrimEnd ( '\' )
}
It 'Get-ChildItem2 should list the content at the root of the mapped drive' {
$result = @ ( Get-ChildItem2 -Path $driveRoot -ErrorAction Stop )
$result . Name | Should -Contain 'Existing.txt'
}
It 'Test-Path2 should find the root of the mapped drive as a container' {
Test-Path2 -Path $driveRoot -PathType Container -ErrorAction Stop | Should -BeTrue
}
It 'Copy-Item2 should copy a file to the root of the mapped drive' {
$source = New-TestSandboxItem -Sandbox $driveSandbox -Name 'CopyToDriveRootSource'
Set-Content -LiteralPath $source -Value 'CopyToRoot'
$result = Copy-Item2 -Path $source -Destination $driveRoot -PassThru $true -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$copied = Join-Path -Path $mapped -ChildPath ( Split-Path -Path $source -Leaf )
Get-Content -LiteralPath $copied | Should -Be 'CopyToRoot'
$source | Should -Exist
}
It 'Move-Item2 should move a file to the root of the mapped drive' {
$source = New-TestSandboxItem -Sandbox $driveSandbox -Name 'MoveToDriveRootSource'
Set-Content -LiteralPath $source -Value 'MoveToRoot'
$result = Move-Item2 -Path $source -Destination $driveRoot -PassThru $true -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$source | Should -Not -Exist
$moved = Join-Path -Path $mapped -ChildPath ( Split-Path -Path $source -Leaf )
Get-Content -LiteralPath $moved | Should -Be 'MoveToRoot'
}
}
Describe 'Omitted -Path falls back to the current location' {
BeforeAll {
$currentLocationFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'OmittedPathLocation' -Directory
$currentLocationFile = Join-Path -Path $currentLocationFolder -ChildPath 'Child.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $currentLocationFile
Set-Content -LiteralPath $currentLocationFile -Value 'Child'
}
It 'Get-Item2 should return the folder of the current location' {
Push-Location -LiteralPath $currentLocationFolder
try {
$result = Get-Item2 -ErrorAction Stop
}
finally {
Pop-Location
}
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$result . FullName . TrimEnd ( '\' ) | Should -Be $currentLocationFolder . TrimEnd ( '\' )
}
It 'Get-ChildItem2 should list the content of the current location' {
Push-Location -LiteralPath $currentLocationFolder
try {
$result = @ ( Get-ChildItem2 -ErrorAction Stop )
}
finally {
Pop-Location
}
$result | Should -HaveCount 1
$result [ 0 ] . FullName | Should -Be $currentLocationFile
}
}
Describe 'Get-ChildItem2 parameter interactions' {
BeforeAll {
$interactionFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'ParamInteractions' -Directory
$interactionSubFolder = Join-Path -Path $interactionFolder -ChildPath 'Sub'
$interactionTopFile = Join-Path -Path $interactionFolder -ChildPath 'Top.txt'
$interactionNestedFile = Join-Path -Path $interactionSubFolder -ChildPath 'Nested.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $interactionSubFolder , $interactionTopFile , $interactionNestedFile
New-Item -ItemType Directory -Path $interactionSubFolder | Out-Null
Set-Content -LiteralPath $interactionTopFile -Value 'Top'
Set-Content -LiteralPath $interactionNestedFile -Value 'Nested'
$attributeFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'AttributesIgnored' -Directory
$hiddenSystemFile = Join-Path -Path $attributeFolder -ChildPath 'HiddenSystem.txt'
$systemFile = Join-Path -Path $attributeFolder -ChildPath 'System.txt'
$hiddenFile = Join-Path -Path $attributeFolder -ChildPath 'Hidden.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $hiddenSystemFile , $systemFile , $hiddenFile
Set-Content -LiteralPath $hiddenSystemFile -Value 'HS'
Set-Content -LiteralPath $systemFile -Value 'S'
Set-Content -LiteralPath $hiddenFile -Value 'H'
[IO.File] :: SetAttributes ( $hiddenSystemFile , [IO.FileAttributes] 'Hidden, System' )
[IO.File] :: SetAttributes ( $systemFile , [IO.FileAttributes] 'System' )
[IO.File] :: SetAttributes ( $hiddenFile , [IO.FileAttributes] 'Hidden' )
}
It 'Should resolve a relative -Path against the current location' {
Push-Location -LiteralPath $interactionFolder
try {
$result = @ ( Get-ChildItem2 -Path ( Split-Path -Path $interactionSubFolder -Leaf ) -ErrorAction Stop )
}
finally {
Pop-Location
}
$result | Should -HaveCount 1
$result [ 0 ] . FullName | Should -Be $interactionNestedFile
}
It 'Should return only the folder when -Directory and -File are given together' {
$result = @ ( Get-ChildItem2 -Path $interactionFolder -Directory -File -ErrorAction Stop )
$result | Should -HaveCount 1
$result [ 0 ] | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$result [ 0 ] . FullName | Should -Be $interactionSubFolder
}
# The cmdlet page documents that -Attributes makes the cmdlet ignore -Force, -Hidden, -System, and -ReadOnly, and
# that it returns a matching hidden item without -Force; each row returns the same names as -Attributes alone,
# which these switches would narrow or widen if they were not ignored.
It 'Should return the same matches for -Attributes <AttributesValue> regardless of -Force, -Hidden, -System, and -ReadOnly (<Case>)' -ForEach @ (
@ { Case = 'alone' ; AttributesValue = 'System' ; Switches = @ { } }
@ { Case = 'with every other switch set' ; AttributesValue = 'System' ; Switches = @ { Force = $true ; Hidden = $true ; System = $true ; ReadOnly = $true } }
@ { Case = 'alone, matching a hidden item without -Force' ; AttributesValue = 'Hidden' ; Switches = @ { } }
@ { Case = 'with -ReadOnly set' ; AttributesValue = 'Hidden' ; Switches = @ { ReadOnly = $true } }
) {
$expected = if ( $AttributesValue -eq 'System' ) { @ ( 'HiddenSystem.txt' , 'System.txt' ) } else { @ ( 'Hidden.txt' , 'HiddenSystem.txt' ) }
$result = @ ( Get-ChildItem2 -Path $attributeFolder -Attributes $AttributesValue @Switches -ErrorAction Stop )
@ ( $result . Name | Sort-Object ) | Should -Be $expected
}
It 'Should have no effect without -Recurse' {
$withDepth = @ ( Get-ChildItem2 -Path $interactionFolder -Depth 0 -ErrorAction Stop )
$withoutDepth = @ ( Get-ChildItem2 -Path $interactionFolder -ErrorAction Stop )
@ ( $withDepth . Name | Sort-Object ) | Should -Be @ ( 'Sub' , 'Top.txt' )
@ ( $withDepth . Name | Sort-Object ) | Should -Be @ ( $withoutDepth . Name | Sort-Object )
}
It 'Should list the same items through the dir2 alias' {
$result = @ ( dir2 -Path $interactionFolder -ErrorAction Stop )
@ ( $result . Name | Sort-Object ) | Should -Be @ ( 'Sub' , 'Top.txt' )
}
It 'Should write AttributesEmpty with its category and target for an empty -Attributes value' {
$caught = $null
try {
Get-ChildItem2 -Path $interactionFolder -Attributes 0 -ErrorAction Stop
}
catch {
$caught = $_
}
$caught . FullyQualifiedErrorId | Should -Be 'AttributesEmpty,NTFSSecurity.GetChildItem2'
$caught . CategoryInfo . Category | Should -Be 'InvalidArgument'
$caught . TargetObject | Should -Be ( [IO.FileAttributes] 0 )
}
}
Describe 'Copy-Item2 destination conflicts and output' {
It 'Should replace an existing file directly with -Force, with PassThru=<_>' -ForEach @ ( $false , $true ) {
$sourceFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'CopyForceSource' -Directory
$destinationFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'CopyForceDestination' -Directory
$source = Join-Path -Path $sourceFolder -ChildPath 'Replace.txt'
$target = Join-Path -Path $destinationFolder -ChildPath 'Replace.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $source , $target
Set-Content -LiteralPath $source -Value 'NewContent'
Set-Content -LiteralPath $target -Value 'OldContent'
$result = @ ( Copy-Item2 -Path $source -Destination $destinationFolder -Force -PassThru $_ -ErrorAction Stop )
Get-Content -LiteralPath $target | Should -Be 'NewContent'
$source | Should -Exist
if ( $_ ) {
$result | Should -HaveCount 1
$result [ 0 ] . FullName | Should -Be $target
}
else {
$result | Should -BeNullOrEmpty
}
}
# The folder case of the same conflict (ItemCmdlets.Tests.ps1:633) already asserts ID, category, and target; this
# file case was asserted by error count only.
It 'Should write DestinationFileAlreadyExists with its category and target for a file that exists at the destination' {
$source = New-TestSandboxItem -Sandbox $sandbox -Name 'FileConflictSource'
$destinationFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FileConflictDestination' -Directory
$existing = Join-Path -Path $destinationFolder -ChildPath ( Split-Path -Path $source -Leaf )
Assert-TestSandboxPath -Sandbox $sandbox -Path $existing
Set-Content -LiteralPath $existing -Value 'Existing'
Copy-Item2 -Path $source -Destination $destinationFolder -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -HaveCount 1
$itemErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'DestinationFileAlreadyExists,*'
$itemErrors [ 0 ] . CategoryInfo . Category | Should -Be 'ResourceExists'
$itemErrors [ 0 ] . TargetObject | Should -Be $existing
Get-Content -LiteralPath $existing | Should -Be 'Existing'
}
It '-PassThru should return the output type for a file and for a folder, and write nothing without -PassThru' {
$sourceFile = New-TestSandboxItem -Sandbox $sandbox -Name 'PassThruTypeFile'
$sourceFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'PassThruTypeFolder' -Directory
$destination = New-TestSandboxItem -Sandbox $sandbox -Name 'PassThruTypeDestination' -Directory
$fileResult = @ ( Copy-Item2 -Path $sourceFile -Destination $destination -PassThru $true -ErrorAction Stop )
$folderResult = @ ( Copy-Item2 -Path $sourceFolder -Destination ( Join-Path -Path $destination -ChildPath 'CopiedFolder' ) -PassThru $true -ErrorAction Stop )
$quietResult = @ ( Copy-Item2 -Path $sourceFile -Destination ( Join-Path -Path $destination -ChildPath 'Quiet' ) -ErrorAction Stop )
$fileResult | Should -HaveCount 1
$fileResult [ 0 ] | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$folderResult | Should -HaveCount 1
$folderResult [ 0 ] | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$quietResult | Should -BeNullOrEmpty
}
}
Describe 'Move-Item2 folder trees, in-place renames, and -Force' {
It 'Should move a non-empty folder tree to a new destination' {
$sourceFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'MoveTreeSource' -Directory
$nestedFolder = Join-Path -Path $sourceFolder -ChildPath 'Nested'
$topFile = Join-Path -Path $sourceFolder -ChildPath 'Top.txt'
$nestedFile = Join-Path -Path $nestedFolder -ChildPath 'Inner.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $nestedFolder , $topFile , $nestedFile
New-Item -ItemType Directory -Path $nestedFolder | Out-Null
Set-Content -LiteralPath $topFile -Value 'Top'
Set-Content -LiteralPath $nestedFile -Value 'Inner'
$destinationParent = New-TestSandboxItem -Sandbox $sandbox -Name 'MoveTreeDestinationParent' -Directory
$result = Move-Item2 -Path $sourceFolder -Destination $destinationParent -PassThru $true -ErrorAction Stop
$movedFolder = Join-Path -Path $destinationParent -ChildPath ( Split-Path -Path $sourceFolder -Leaf )
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$sourceFolder | Should -Not -Exist
Get-Content -LiteralPath ( Join-Path -Path $movedFolder -ChildPath 'Top.txt' ) | Should -Be 'Top'
Get-Content -LiteralPath ( Join-Path -Path $movedFolder -ChildPath 'Nested\Inner.txt' ) | Should -Be 'Inner'
}
It 'Should rename an item in place on the same volume' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RenameSource'
Set-Content -LiteralPath $file -Value 'RenameContent'
$renamed = Join-Path -Path ( Split-Path -Path $file -Parent ) -ChildPath 'RenameTarget.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $renamed
$result = Move-Item2 -Path $file -Destination $renamed -PassThru $true -ErrorAction Stop
$file | Should -Not -Exist
$renamed | Should -Exist
Get-Content -LiteralPath $renamed | Should -Be 'RenameContent'
$result . FullName | Should -Be $renamed
}
# Unlike Copy-Item2 -Force, which merges into an existing folder of the same name (ItemCmdlets.Tests.ps1:681),
# Move-Item2 -Force replaces it outright: run first with diagnostics instead of an assumed error (the
# destinationExists check of MoveItem2.cs is bypassed by -Force, since it is guarded by "!force"), the existing
# folder at the destination was gone afterwards, together with the file it held, and the moved folder took its
# place with no error, which this pins as observed.
It 'Should replace an existing folder outright with -Force, not merge it, and write no error' {
$sourceFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'MoveForceFolderSource' -Directory
$destinationParent = New-TestSandboxItem -Sandbox $sandbox -Name 'MoveForceFolderDestinationParent' -Directory
$existingFolder = Join-Path -Path $destinationParent -ChildPath ( Split-Path -Path $sourceFolder -Leaf )
Assert-TestSandboxPath -Sandbox $sandbox -Path $existingFolder
New-Item -ItemType Directory -Path $existingFolder | Out-Null
$sourceContent = Join-Path -Path $sourceFolder -ChildPath 'FromSource.txt'
$existingContent = Join-Path -Path $existingFolder -ChildPath 'AlreadyThere.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $sourceContent , $existingContent
Set-Content -LiteralPath $sourceContent -Value 'FromSource'
Set-Content -LiteralPath $existingContent -Value 'AlreadyThere'
$result = Move-Item2 -Path $sourceFolder -Destination $destinationParent -Force -PassThru $true -ErrorVariable itemErrors -ErrorAction SilentlyContinue
$itemErrors | Should -BeNullOrEmpty
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$result . FullName | Should -Be $existingFolder
[IO.Directory] :: Exists ( $sourceFolder ) | Should -BeFalse
Get-Content -LiteralPath ( Join-Path -Path $existingFolder -ChildPath 'FromSource.txt' ) | Should -Be 'FromSource'
[IO.File] :: Exists ( $existingContent ) | Should -BeFalse
}
}
Describe 'Remove-Item2 defaults, aliases, and error details' {
It 'Should do nothing when -Path is omitted' {
$omittedPathFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveOmittedPath' -Directory
$kept = Join-Path -Path $omittedPathFolder -ChildPath 'Kept.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $kept
Set-Content -LiteralPath $kept -Value 'Kept'
Push-Location -LiteralPath $omittedPathFolder
try {
$result = @ ( Remove-Item2 -PassThru -ErrorVariable removeErrors -ErrorAction SilentlyContinue )
}
finally {
Pop-Location
}
$removeErrors | Should -BeNullOrEmpty
$result | Should -BeNullOrEmpty
$kept | Should -Exist
$omittedPathFolder | Should -Exist
}
It 'Should remove the file through the <Alias> alias' -ForEach @ (
@ { Alias = 'rm2' }
@ { Alias = 'del2' }
) {
$file = New-TestSandboxItem -Sandbox $sandbox -Name " RemoveAlias $ Alias "
& $Alias -Path $file -ErrorAction Stop
$file | Should -Not -Exist
}
It 'Should write FileNotFound with its category and target for a path that does not exist' {
$missing = Join-Path -Path $sandbox -ChildPath ( 'RemoveMissing-{0}' -f [guid] :: NewGuid ( ) . ToString ( 'N' ) . Substring ( 0 , 8 ) )
Remove-Item2 -Path $missing -ErrorVariable removeErrors -ErrorAction SilentlyContinue
$removeErrors | Should -HaveCount 1
$removeErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*'
$removeErrors [ 0 ] . CategoryInfo . Category | Should -Be 'ObjectNotFound'
$removeErrors [ 0 ] . TargetObject | Should -Be $missing
}
It 'Should write nothing for a successful removal without -PassThru' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveQuiet'
$result = @ ( Remove-Item2 -Path $file -ErrorAction Stop )
$result | Should -BeNullOrEmpty
$file | Should -Not -Exist
}
# The generic catch of RemoveItem2.cs (category NotSpecified) is reached by anything that is not an IOException;
# a Deny ACE for Delete makes the file system refuse with an UnauthorizedAccessException, a sibling of
# IOException, not a subclass of it, so it falls through to that generic branch instead of the InvalidData one
# that the locked-file and non-empty-folder cases already cover (Remove-Item2.Tests.ps1:36,97). NTFS allows a
# delete when either the file itself allows it or its parent grants DeleteSubdirectoriesAndFiles, so a dedicated
# folder denies the second route as well; otherwise the inherited folder permission of the sandbox still lets the
# delete through despite the Deny entry on the file alone (seen first-hand: no error was written).
It 'Should write DeleteError with category NotSpecified when a Deny ACE refuses the delete' {
$deniedFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'RemoveAccessDeniedFolder' -Directory
$file = Join-Path -Path $deniedFolder -ChildPath 'Denied.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file
Set-Content -LiteralPath $file -Value 'Denied'
Add-TestDenyRule -Sandbox $sandbox -Path $deniedFolder -Rights @ { 'S-1-1-0' = 'DeleteSubdirectoriesAndFiles' }
Add-TestDenyRule -Sandbox $sandbox -Path $file -Rights @ { 'S-1-1-0' = 'Delete' }
Remove-Item2 -Path $file -ErrorVariable removeErrors -ErrorAction SilentlyContinue
$removeErrors | Should -HaveCount 1
$removeErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'DeleteError,*'
$removeErrors [ 0 ] . CategoryInfo . Category | Should -Be 'NotSpecified'
$removeErrors [ 0 ] . TargetObject | Should -Be $file
$removeErrors [ 0 ] . Exception | Should -BeOfType [System.UnauthorizedAccessException]
$file | Should -Exist
}
}
Describe 'Test-Path2 with several path shapes' {
It 'Should accept several piped objects and bind -PathType by property name for each' {
$pipedFile = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedPathTypeFile'
$pipedFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'PipedPathTypeFolder' -Directory
$requests = @ (
[pscustomobject] @ { FullName = $pipedFile ; PathType = 'Leaf' }
[pscustomobject] @ { FullName = $pipedFile ; PathType = 'Container' }
[pscustomobject] @ { FullName = $pipedFolder ; PathType = 'Container' }
)
$result = @ ( $requests | Test-Path2 -ErrorAction Stop )
$result -join ',' | Should -Be 'True,False,True'
}
It 'Should find an item whose name contains brackets' {
$bracketFile = Join-Path -Path $sandbox -ChildPath 'TestPathBracket[2].txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $bracketFile
Set-Content -LiteralPath $bracketFile -Value 'Bracket'
Test-Path2 -Path $bracketFile -PathType Leaf -ErrorAction Stop | Should -BeTrue
}
It 'Should find an item through a UNC path of the administrative share' -Skip : ( -not $canUseAdminShare ) {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'UncPathFile'
$uncPath = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $file
Test-Path2 -Path $uncPath -PathType Leaf -ErrorAction Stop | Should -BeTrue
}
}
Describe 'Get-FileHash2 binds -Algorithm by property name' {
It 'Should hash with the algorithm bound by property name from the pipeline' {
# Stored in a variable, like FileHash.Tests.ps1 and the MD5 call above: PSScriptAnalyzer flags a literal
# weak-algorithm argument, not one that comes from a variable.
$propertyAlgorithm = 'MD5'
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'HashAlgorithmByProperty'
Set-Content -LiteralPath $file -Value 'AlgorithmPropertyContent'
$request = [pscustomobject] @ { FullName = $file ; Algorithm = $propertyAlgorithm }
$result = $request | Get-FileHash2 -ErrorAction Stop
$result . Algorithm | Should -Be $propertyAlgorithm
$result . Hash | Should -BeExactly ( Get-FileHash -LiteralPath $file -Algorithm $propertyAlgorithm ) . Hash
}
}
Describe 'Get-DiskSpace free-space values and validation error' {
It 'Should report free-space values close to an independent .NET read for the system drive' {
$before = New-Object -TypeName 'System.IO.DriveInfo' -ArgumentList $env:SystemDrive
$result = Get-DiskSpace -DriveLetter $env:SystemDrive -ErrorAction Stop
$after = New-Object -TypeName 'System.IO.DriveInfo' -ArgumentList $env:SystemDrive
# Free space can change between two live reads of a system drive under normal system activity, so the
# comparison admits a tolerance around the values read immediately before and after the call, instead of
# exact equality; the tolerance is still tight enough to catch a swapped or a wrong field.
$tolerance = 500MB
$result . TotalNumberOfFreeBytes | Should -BeGreaterOrEqual ( [Math] :: Min ( $before . TotalFreeSpace , $after . TotalFreeSpace ) - $tolerance )
$result . TotalNumberOfFreeBytes | Should -BeLessOrEqual ( [Math] :: Max ( $before . TotalFreeSpace , $after . TotalFreeSpace ) + $tolerance )
$result . FreeBytesAvailable | Should -BeGreaterOrEqual ( [Math] :: Min ( $before . AvailableFreeSpace , $after . AvailableFreeSpace ) - $tolerance )
$result . FreeBytesAvailable | Should -BeLessOrEqual ( [Math] :: Max ( $before . AvailableFreeSpace , $after . AvailableFreeSpace ) + $tolerance )
}
# PowerShell's own parameter-validation engine raises this error before the cmdlet runs, so unlike the cmdlet's
# own WriteError calls it carries no target object; this pins that engine behavior as observed, not assumed.
It 'Should write the validation error for a bad -DriveLetter with its category, and no target object' {
$caught = $null
try {
Get-DiskSpace -DriveLetter 'C' -ErrorAction Stop
}
catch {
$caught = $_
}
$caught . FullyQualifiedErrorId | Should -Be 'ParameterArgumentValidationError,NTFSSecurity.GetDiskSpace'
$caught . CategoryInfo . Category | Should -Be 'InvalidData'
$caught . TargetObject | Should -BeNullOrEmpty
}
}
Describe 'New-NTFSHardLink positional binding, FileInfo input, and share errors' {
It 'Should bind -Path and -Target positionally' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'PositionalHardLinkTarget'
Set-Content -LiteralPath $target -Value 'PositionalTarget'
$link = Join-Path -Path $sandbox -ChildPath 'PositionalHardLinkLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSHardLink $link $target -ErrorAction Stop
$link | Should -Exist
Get-Content -LiteralPath $link | Should -Be 'PositionalTarget'
}
# Target has no FullName alias, so a FileInfo object can reach it only by value, through the
# FileSystemPathTransformationAttribute of BaseCmdlets.cs, which reads .FullName for a FileSystemInfo argument.
# -Path is bound explicitly by name here, which keeps it out of the pipeline-by-value binding so that the piped
# FileInfo binds to -Target only.
It 'Should accept a FileInfo object piped by value for -Target' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'FileInfoBoundTarget'
Set-Content -LiteralPath $target -Value 'FileInfoTarget'
$targetItem = Get-Item2 -Path $target -ErrorAction Stop
$link = Join-Path -Path $sandbox -ChildPath 'FileInfoBoundLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$targetItem | New-NTFSHardLink -Path $link -ErrorAction Stop
$link | Should -Exist
Get-Content -LiteralPath $link | Should -Be 'FileInfoTarget'
}
# The existing share test (Links.Tests.ps1:150) asserts only the ID of this error.
It '-PassThru should write GetHardLinkError with category ReadError and the link path as target on a network share' -Skip : ( -not $canUseAdminShare ) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'ShareCategoryTarget'
$link = Join-Path -Path $sandbox -ChildPath 'ShareCategoryLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$sharePath = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $link
$shareTarget = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $target
$result = @ ( New-NTFSHardLink -Path $sharePath -Target $shareTarget -PassThru -ErrorVariable linkErrors -ErrorAction SilentlyContinue )
$link | Should -Exist
$linkErrors | Should -HaveCount 1
$linkErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*'
$linkErrors [ 0 ] . CategoryInfo . Category | Should -Be 'ReadError'
$linkErrors [ 0 ] . TargetObject | Should -Be $sharePath
$result | Should -BeNullOrEmpty
}
}
Describe 'Get-NTFSHardLink category on a network share' {
# The existing share test (Links.Tests.ps1:247) asserts the ID and the target but not the category.
It 'Should write GetHardLinkError with category ReadError for a file on a network share' -Skip : ( -not $canUseAdminShare ) {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'HardLinkShareCategoryFile'
$sharePath = ConvertTo-TestAdminSharePath -Sandbox $sandbox -Path $file
Get-NTFSHardLink -Path $sharePath -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 1
$linkErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'GetHardLinkError,*'
$linkErrors [ 0 ] . CategoryInfo . Category | Should -Be 'ReadError'
$linkErrors [ 0 ] . TargetObject | Should -Be $sharePath
}
}
Describe 'New-NTFSSymbolicLink positional binding and folder links' {
It 'Should bind -Path and -Target positionally' -Skip : ( -not $canCreateSymbolicLinks ) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'PositionalSymbolicTarget'
Set-Content -LiteralPath $target -Value 'PositionalSymbolicTarget'
$link = Join-Path -Path $sandbox -ChildPath 'PositionalSymbolicLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSSymbolicLink $link $target -ErrorAction Stop
( Get-Item -LiteralPath $link -Force ) . LinkType | Should -Be 'SymbolicLink'
Get-Content -LiteralPath $link | Should -Be 'PositionalSymbolicTarget'
}
# The existing folder-link test (Links.Tests.ps1:304) checks only the Directory attribute and reachability; only
# the file-link test (Links.Tests.ps1:297) asserts LinkType and the resolved target.
It 'Should set LinkType and the resolved target for a link to a folder' -Skip : ( -not $canCreateSymbolicLinks ) {
$targetFolder = New-TestSandboxItem -Sandbox $sandbox -Name 'FolderLinkTypeTarget' -Directory
$link = Join-Path -Path $sandbox -ChildPath 'FolderLinkType'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSSymbolicLink -Path $link -Target $targetFolder -ErrorAction Stop
( Get-Item -LiteralPath $link -Force ) . LinkType | Should -Be 'SymbolicLink'
# Windows PowerShell returns the target as an array, PowerShell 7 as a string (Links.Tests.ps1:286).
@ ( ( Get-Item -LiteralPath $link -Force ) . Target ) [ 0 ] | Should -Be $targetFolder
}
}
Describe 'Enable-Privileges -PassThru privilege states' {
AfterEach {
# Leaves the process token as the rest of the file expects it, regardless of which assertion above failed.
Disable-Privileges -ErrorAction SilentlyContinue -WarningAction SilentlyContinue | Out-Null
}
# Only Get-Privileges was used as the read-back before; this reads the four states directly from the -PassThru
# objects and cross-checks them against whoami /priv, an independent source outside the module.
It 'Should report Enabled for TakeOwnership, Restore, Backup, and Security in -PassThru, matching whoami /priv' -Skip : ( -not $isElevated ) {
$result = @ ( Enable-Privileges -PassThru -ErrorAction Stop )
$whoamiPrivileges = & whoami . exe / priv / fo csv | Select-Object -Skip 1 | ConvertFrom-Csv -Header 'Name' , 'Description' , 'State'
foreach ( $name in 'TakeOwnership' , 'Restore' , 'Backup' , 'Security' ) {
$entry = @ ( $result | Where-Object -Property Privilege -EQ -Value $name )
$entry | Should -HaveCount 1 -Because " Enable-Privileges -PassThru should list $ name "
$entry [ 0 ] . PrivilegeState | Should -Be 'Enabled'
$whoamiEntry = @ ( $whoamiPrivileges | Where-Object -Property Name -EQ -Value ( 'Se{0}Privilege' -f $name ) )
# The State column is localized; only the rows that whoami prints in English are compared (Get-Privileges
# compared with an independent source, above).
if ( $whoamiEntry . Count -gt 0 -and $whoamiEntry [ 0 ] . State -in 'Enabled' , 'Disabled' ) {
$whoamiEntry [ 0 ] . State | Should -Be 'Enabled'
}
}
}
}
Describe 'Disable-Privileges -PassThru privilege states' {
It 'Should report Disabled for TakeOwnership, Restore, and Backup in -PassThru after a successful disable' -Skip : ( -not $isElevated ) {
Enable-Privileges -ErrorAction Stop
$result = @ ( Disable-Privileges -PassThru -ErrorAction Stop )
foreach ( $name in 'TakeOwnership' , 'Restore' , 'Backup' ) {
$entry = @ ( $result | Where-Object -Property Privilege -EQ -Value $name )
$entry | Should -HaveCount 1 -Because " Disable-Privileges -PassThru should list $ name "
$entry [ 0 ] . PrivilegeState | Should -Be 'Disabled'
}
}
}
# NOT DONE: "the Removed PrivilegeState value" (WORKLIST row 14) is not reachable through Get-Privileges. A probe
# (elevated, PowerShell 7) that removed the Backup privilege of a child process and compared Get-Privileges before
# and after showed the entry disappear from the list entirely instead of remaining with PrivilegeState=Removed:
# BEFORE: ...,Backup=0,Restore=0,... AFTER: ...,Restore=0,... (Backup is gone, not listed as Removed)
# Privileges.GetPrivileges(accessTokenHandle) (ProcessPrivileges\Privileges.cs) builds its collection only from what
# GetTokenInformation(TokenPrivileges) currently returns, which drops an entry once SE_PRIVILEGE_REMOVED takes effect
# (matching the documented Win32 behavior of that attribute); the Removed state is produced only by the unrelated
# Privileges.GetPrivilegeAttributes(privilege, collection) lookup helper, which Get-Privileges never calls. No input
# makes Get-Privileges itself emit a Removed entry, so this item is left out instead of pinning a test that cannot
# exercise the branch it names; this is a structural gap of the same kind as the "Limit" rows, not a defect of a
# cmdlet, so nothing is reported under DEFECTS.