Tests/Public/Repair-FileShareIntegrity.Tests.ps1
|
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Tests intentionally swallow errors to verify side-effects')] param() BeforeAll { $modulePath = "$PSScriptRoot/../.." Get-ChildItem -Path "$modulePath/Private/*.ps1" | ForEach-Object { . $_.FullName } Get-ChildItem -Path "$modulePath/Public/*.ps1" | ForEach-Object { . $_.FullName } } Describe 'Repair-FileShareIntegrity' { BeforeEach { Mock Write-Host {} Mock Write-Warning {} Mock Assert-AzCliLogin { [PSCustomObject]@{ user = @{ name = 'test@contoso.com' } } } Mock Assert-AzCopyInstalled {} Mock Get-StorageAccountKey { 'fakekey123==' } Mock New-SasToken { 'fakesas=token' } Mock Mount-SmbDrive {} Mock Dismount-SmbDrive {} Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } } Context 'Empty input' { It 'Returns early when no file paths provided' { Repair-FileShareIntegrity -FilePaths @() -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck Should -Invoke Write-Host -ParameterFilter { $Object -like '*No file paths*' } } } Context 'Drive letter validation' { It 'Throws when source and destination drive letters are the same' { { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SourceDriveLetter 'X' -DestinationDriveLetter 'X' -SkipPreCheck } | Should -Throw '*must be different*' } It 'Throws when drive is already mounted' { Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' } { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck } | Should -Throw '*already mounted*' } } Context 'Pre-flight calls' { It 'Calls Assert-AzCliLogin' { try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Assert-AzCliLogin -Times 1 } It 'Calls Assert-AzCopyInstalled when -AzCopy is specified' { try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -AzCopy -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Assert-AzCopyInstalled -Times 1 } It 'Does not call Assert-AzCopyInstalled when -AzCopy is not specified' { try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Assert-AzCopyInstalled -Times 0 } It 'Generates SAS tokens only when -AzCopy is specified' { try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -AzCopy -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke New-SasToken -Times 2 # Reset invocation counts try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} # Should only be 2 from the first call, not additional ones } } Context 'Deduplication' { It 'Deduplicates file paths case-insensitively' { try { Repair-FileShareIntegrity -FilePaths @('data\File.txt', 'data\file.txt', 'DATA\FILE.TXT') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} # Should report 1 file path (deduplicated) Should -Invoke Write-Host -ParameterFilter { $Object -like '*1 file path*' } } } Context 'Pipeline input' { It 'Accepts file paths from the pipeline' { try { @('file1.txt', 'file2.txt') | Repair-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Write-Host -ParameterFilter { $Object -like '*2 file path*' } } } Context 'Cleanup on failure' { It 'Dismounts source drive when destination mount fails' { # Source mount succeeds (sets $mountedSource = $true), destination mount throws Mock Mount-SmbDrive {} -ParameterFilter { $DriveLetter -eq 'X' } Mock Mount-SmbDrive { throw 'Mount failed' } -ParameterFilter { $DriveLetter -eq 'Y' } try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Dismount-SmbDrive -ParameterFilter { $DriveLetter -eq 'X' } } } Context 'Pre-check phase' { It 'Skips pre-check when -SkipPreCheck is specified' { try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Write-Host -ParameterFilter { $Object -like '*Phase 1: Skipped*' } } It 'Mounts drives for pre-check when -SkipPreCheck is not specified' { Mock Test-FileByteIntegrity { 'MissingOnSource' } try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Mount-SmbDrive -Times 2 } } Context 'WhatIf support' { It 'Does not copy files when -WhatIf is specified' { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -WhatIf Should -Invoke Mount-SmbDrive -Times 0 } } Context 'Parameter validation' { It 'Drive letter must be a single alpha character' { { Repair-FileShareIntegrity -FilePaths @('f.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SourceDriveLetter '12' } | Should -Throw } It 'SourceAccountName is mandatory' { $param = (Get-Command Repair-FileShareIntegrity).Parameters['SourceAccountName'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object { $_.Mandatory | Should -BeTrue } } It 'DestinationAccountName is mandatory' { $param = (Get-Command Repair-FileShareIntegrity).Parameters['DestinationAccountName'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object { $_.Mandatory | Should -BeTrue } } It 'ShareName is mandatory' { $param = (Get-Command Repair-FileShareIntegrity).Parameters['ShareName'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object { $_.Mandatory | Should -BeTrue } } It 'SupportsShouldProcess is enabled' { $cmd = Get-Command Repair-FileShareIntegrity $cmdBindingAttr = $cmd.ScriptBlock.Attributes | Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] } $cmdBindingAttr.SupportsShouldProcess | Should -BeTrue } It 'FilePaths accepts pipeline input' { $param = (Get-Command Repair-FileShareIntegrity).Parameters['FilePaths'] $pipelineAttr = $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] -and $_.ValueFromPipeline } $pipelineAttr | Should -Not -BeNullOrEmpty } } Context 'Null/empty pipeline paths filtered' { It 'Filters out null and empty paths from pipeline' { # Pipeline input with empty/null values causes binding error since FilePaths is [string[]] # Verify that only valid string paths are accepted { @('', $null) | Repair-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } | Should -Throw } } Context 'Pre-check matching files' { BeforeEach { # Create temp PSDrives so Join-Path doesn't fail when resolving X:\ and Y:\ if (-not (Get-PSDrive -Name 'X' -ErrorAction SilentlyContinue)) { New-PSDrive -Name 'X' -PSProvider FileSystem -Root $TestDrive -Scope Script | Out-Null } if (-not (Get-PSDrive -Name 'Y' -ErrorAction SilentlyContinue)) { New-PSDrive -Name 'Y' -PSProvider FileSystem -Root $TestDrive -Scope Script | Out-Null } } AfterEach { Remove-PSDrive -Name 'X' -ErrorAction SilentlyContinue Remove-PSDrive -Name 'Y' -ErrorAction SilentlyContinue } It 'Skips files that already match during pre-check' { Mock Test-FileByteIntegrity { 'Match' } try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke Write-Host -ParameterFilter { $Object -like '*No files need remediation*' } } It 'Skips files missing on source during pre-check' { Mock Test-FileByteIntegrity { 'MissingOnSource' } try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -LogDirectory $TestDrive -Confirm:$false } catch {} # File should be skipped, so no remediation needed Should -Invoke Write-Host -ParameterFilter { $Object -like '*No files need remediation*' } } It 'Handles pre-check errors gracefully' { Mock Test-FileByteIntegrity { throw 'Access denied' } try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -LogDirectory $TestDrive -Confirm:$false } catch {} # Error is counted, file is not added to remediation Should -Invoke Write-Host -ParameterFilter { $Object -like '*No files need remediation*' } } } Context 'AzCopy mode pre-flight' { It 'Generates SAS tokens with correct permissions for AzCopy mode' { try { Repair-FileShareIntegrity -FilePaths @('file.txt') -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -AzCopy -SkipPreCheck -LogDirectory $TestDrive -Confirm:$false } catch {} Should -Invoke New-SasToken -ParameterFilter { $Permissions -eq 'rl' } -Times 1 Should -Invoke New-SasToken -ParameterFilter { $Permissions -eq 'rwdlc' } -Times 1 } } } |