diff --git a/Tests/Links.Tests.ps1 b/Tests/Links.Tests.ps1 index f75509a..8d69a92 100644 --- a/Tests/Links.Tests.ps1 +++ b/Tests/Links.Tests.ps1 @@ -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 + } }