@ -6,6 +6,11 @@
) ]
param ( )
BeforeDiscovery {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
$canCreateSymbolicLinks = Test-PrivilegeHeld -Name 'SeCreateSymbolicLinkPrivilege'
}
BeforeAll {
Import-Module -Name ( Join-Path -Path $PSScriptRoot -ChildPath 'TestHelpers.psm1' ) -Force
$modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\NTFSSecurity\bin\Release\NTFSSecurity.psd1'
@ -41,4 +46,225 @@ Describe 'New-NTFSHardLink' {
{ New-NTFSHardLink -Path $link -Target $missing -ErrorAction Stop } | Should -Throw -ExpectedMessage '*does not exist*'
$link | Should -Not -Exist
}
It 'Should write nothing without -PassThru' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'Quiet'
$link = Join-Path -Path $sandbox -ChildPath 'QuietLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$result = @ ( New-NTFSHardLink -Path $link -Target $target -ErrorAction Stop )
$result | Should -BeNullOrEmpty
}
It 'Should return every name of the file with -PassThru' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'Names'
$link = Join-Path -Path $sandbox -ChildPath 'NamesLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$result = @ ( New-NTFSHardLink -Path $link -Target $target -PassThru -ErrorAction Stop )
$result | Should -HaveCount 2
$result | ForEach-Object -Process { $_ | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo] }
( $result . FullName | Sort-Object ) -join '|' | Should -Be ( ( @ ( $target , $link ) | Sort-Object ) -join '|' )
$result [ 0 ] . Mode | Should -Not -BeNullOrEmpty
}
It 'Should give both names the same data' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'Shared'
$link = Join-Path -Path $sandbox -ChildPath 'SharedLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSHardLink -Path $link -Target $target -ErrorAction Stop
Set-Content -LiteralPath $link -Value 'Changed through the link'
Get-Content -LiteralPath $target | Should -Be 'Changed through the link'
}
It 'Should resolve relative paths against the current location' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'Relative'
$link = Join-Path -Path $sandbox -ChildPath 'RelativeLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSHardLink -Path 'RelativeLink.txt' -Target ( Split-Path -Path $target -Leaf ) -ErrorAction Stop
$link | Should -Exist
}
It 'Should refuse an existing -Path and leave it unchanged' {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'ExistingTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'Existing'
Set-Content -LiteralPath $existing -Value 'Existing'
{ New-NTFSHardLink -Path $existing -Target $target -ErrorAction Stop } | Should -Throw -ExpectedMessage '*already exist*'
Get-Content -LiteralPath $existing | Should -Be 'Existing'
}
It 'Should refuse a folder as -Target and create no link' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'FolderTarget' -Directory
$link = Join-Path -Path $sandbox -ChildPath 'FolderLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
{ New-NTFSHardLink -Path $link -Target $folder -ErrorAction Stop } | Should -Throw -ExpectedMessage '*not a file*'
$link | Should -Not -Exist
}
}
Describe 'Get-NTFSHardLink' {
It 'Should return the file itself for a file with one name' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Single'
$result = @ ( Get-NTFSHardLink -Path $file -ErrorAction Stop )
$result | Should -HaveCount 1
$result [ 0 ] . FullName | Should -Be $file
$result [ 0 ] . Mode | Should -Not -BeNullOrEmpty
}
It 'Should return every name of a file with hard links' {
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'Linked'
$link = Join-Path -Path $sandbox -ChildPath 'LinkedLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSHardLink -Path $link -Target $file -ErrorAction Stop
$result = @ ( Get-NTFSHardLink -Path $link -ErrorAction Stop )
( $result . FullName | Sort-Object ) -join '|' | Should -Be ( ( @ ( $file , $link ) | Sort-Object ) -join '|' )
}
It 'Should take the files with more than one name from Get-ChildItem2' {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'Counted' -Directory
$file = Join-Path -Path $folder -ChildPath 'File.txt'
$link = Join-Path -Path $folder -ChildPath 'Link.txt'
$other = Join-Path -Path $folder -ChildPath 'Other.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file , $link , $other
Set-Content -LiteralPath $file , $other -Value 'File'
New-NTFSHardLink -Path $link -Target $file -ErrorAction Stop
$result = @ ( Get-ChildItem2 -Path $folder -File | Where-Object -Property HardLinkCount -GT -Value 1 | Get-NTFSHardLink -ErrorAction Stop )
$result . FullName | Should -Not -Contain $other
@ ( $result . FullName | Sort-Object -Unique ) -join '|' | Should -Be ( ( @ ( $file , $link ) | Sort-Object ) -join '|' )
}
It 'Should write an error for a path that does not exist and continue with the next path' {
$missing = Join-Path -Path $sandbox -ChildPath 'MissingHardLink.txt'
$file = New-TestSandboxItem -Sandbox $sandbox -Name 'AfterMissing'
$result = @ ( Get-NTFSHardLink -Path $missing , $file -ErrorVariable linkErrors -ErrorAction SilentlyContinue )
$linkErrors | Should -HaveCount 1
$linkErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'FileNotFound,*'
$result . FullName | Should -Be $file
}
}
Describe 'New-NTFSSymbolicLink' {
BeforeAll {
function Get-LinkTarget {
param ( [string] $Path )
# Windows PowerShell returns the target as an array, PowerShell 7 as a string.
@ ( ( Get-Item -LiteralPath $Path -Force ) . Target ) [ 0 ]
}
}
It 'Should create a link to a file that reads the data of the file' -Skip : ( -not $canCreateSymbolicLinks ) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicTarget'
Set-Content -LiteralPath $target -Value 'Target'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicFile.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$result = @ ( New-NTFSSymbolicLink -Path $link -Target $target -ErrorAction Stop )
$result | Should -BeNullOrEmpty
( Get-Item -LiteralPath $link -Force ) . LinkType | Should -Be 'SymbolicLink'
Get-LinkTarget -Path $link | Should -Be $target
Get-Content -LiteralPath $link | Should -Be 'Target'
}
It 'Should create a link to a folder through which the files of the folder are reachable' -Skip : ( -not $canCreateSymbolicLinks ) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicFolder' -Directory
$file = Join-Path -Path $folder -ChildPath 'File.txt'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicFolderLink'
Assert-TestSandboxPath -Sandbox $sandbox -Path $file , $link
Set-Content -LiteralPath $file -Value 'File'
New-NTFSSymbolicLink -Path $link -Target $folder -ErrorAction Stop
( Get-Item -LiteralPath $link -Force ) . Attributes . HasFlag ( [IO.FileAttributes] :: Directory ) | Should -BeTrue
Test-Path2 -Path ( Join-Path -Path $link -ChildPath 'File.txt' ) -PathType Leaf | Should -BeTrue
}
It 'Should return a file object for a link to a file with -PassThru' -Skip : ( -not $canCreateSymbolicLinks ) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicPassThru'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicPassThru.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$result = New-NTFSSymbolicLink -Path $link -Target $target -PassThru -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.FileInfo]
$result . FullName | Should -Be $link
}
# Before 5.0.0, the cmdlet returned a file object for a link to a folder.
It 'Should return a folder object for a link to a folder with -PassThru' -Skip : ( -not $canCreateSymbolicLinks ) {
$folder = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicPassThruFolder' -Directory
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicPassThruFolderLink'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$result = New-NTFSSymbolicLink -Path $link -Target $folder -PassThru -ErrorAction Stop
$result | Should -BeOfType [Alphaleonis.Win32.Filesystem.DirectoryInfo]
$result . FullName | Should -Be $link
}
It 'Should store the absolute path of a relative target' -Skip : ( -not $canCreateSymbolicLinks ) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicRelative'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicRelative.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
New-NTFSSymbolicLink -Path 'SymbolicRelative.txt' -Target ( Split-Path -Path $target -Leaf ) -ErrorAction Stop
Get-LinkTarget -Path $link | Should -Be $target
}
It 'Should refuse an existing -Path and leave it unchanged' -Skip : ( -not $canCreateSymbolicLinks ) {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicExistingTarget'
$existing = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicExisting'
Set-Content -LiteralPath $existing -Value 'Existing'
{ New-NTFSSymbolicLink -Path $existing -Target $target -ErrorAction Stop } | Should -Throw -ExpectedMessage '*already exist*'
( Get-Item -LiteralPath $existing -Force ) . LinkType | Should -BeNullOrEmpty
Get-Content -LiteralPath $existing | Should -Be 'Existing'
}
It 'Should write an error for a target that does not exist and create no link' {
$missing = Join-Path -Path $sandbox -ChildPath 'SymbolicMissing.txt'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicMissingLink.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $missing , $link
New-NTFSSymbolicLink -Path $link -Target $missing -ErrorVariable linkErrors -ErrorAction SilentlyContinue
$linkErrors | Should -HaveCount 1
$linkErrors [ 0 ] . FullyQualifiedErrorId | Should -BeLike 'CreateSymbolicLinkError,*'
Test-Path2 -Path $link | Should -BeFalse
}
# Windows rejects the link with error 1314 without the "Create symbolic links" user right. Its message is
# localized, so the test compares the HRESULT of that error.
It 'Should fail and create no link without the right to create symbolic links' -Skip : $canCreateSymbolicLinks {
$target = New-TestSandboxItem -Sandbox $sandbox -Name 'SymbolicNoRight'
$link = Join-Path -Path $sandbox -ChildPath 'SymbolicNoRight.txt'
Assert-TestSandboxPath -Sandbox $sandbox -Path $link
$thrown = { New-NTFSSymbolicLink -Path $link -Target $target -ErrorAction Stop } | Should -Throw -PassThru
$thrown . FullyQualifiedErrorId | Should -BeLike '*,NTFSSecurity.NewSymbolicLink'
'0x{0:X8}' -f $thrown . Exception . HResult | Should -Be '0x80070522'
Test-Path2 -Path $link | Should -BeFalse
}
}