Tests/Public/Compare-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 'Compare-FileShareIntegrity' { BeforeEach { Mock Write-Host {} Mock Write-Warning {} Mock Assert-AzCliLogin { [PSCustomObject]@{ user = @{ name = 'test@contoso.com' } } } Mock Get-StorageAccountKey { 'fakekey123==' } Mock Mount-SmbDrive {} Mock Dismount-SmbDrive {} } Context 'Mutually exclusive deep-check modes' { It 'Throws when -Partial and -HashAll are both specified' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -Partial -HashAll } | Should -Throw '*Only one of*' } It 'Throws when -Partial and -SampleCount are both specified' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -Partial -SampleCount 100 } | Should -Throw '*Only one of*' } It 'Throws when -HashAll and -SampleCount are both specified' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -HashAll -SampleCount 100 } | Should -Throw '*Only one of*' } } Context 'Drive letter validation' { It 'Throws when source and destination drive letters are the same' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SourceDriveLetter 'X' -DestinationDriveLetter 'X' } | Should -Throw '*must be different*' } It 'Throws when drive is already mounted' { Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' } { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } | Should -Throw '*already mounted*' } } Context 'Pre-flight calls' { BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } # Make Mount-SmbDrive throw to stop execution before file enumeration Mock Mount-SmbDrive { throw 'Intentional stop' } } It 'Calls Assert-AzCliLogin' { try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {} Should -Invoke Assert-AzCliLogin -Times 1 } It 'Retrieves keys for both accounts' { try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {} Should -Invoke Get-StorageAccountKey -Times 2 } } Context 'Share resolution' { BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } # Throw on mount so enumeration never starts Mock Mount-SmbDrive { throw 'Intentional stop' } } It 'Lists all shares when -ShareName is not provided' { Mock -CommandName 'az' -MockWith { $global:LASTEXITCODE = 0 'share1' 'share2' } try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' } catch {} Should -Invoke Mount-SmbDrive -Times 1 -ParameterFilter { $ShareName -eq 'share1' } } } Context 'Cleanup on failure' { BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } Mock Mount-SmbDrive { throw 'Mount failed' } -ParameterFilter { $DriveLetter -eq 'Y' } Mock Mount-SmbDrive {} -ParameterFilter { $DriveLetter -eq 'X' } } It 'Dismounts drives even when an error occurs' { try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {} Should -Invoke Dismount-SmbDrive } } Context 'Parameter validation' { It 'ThrottleLimit must be between 1 and 64' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ThrottleLimit 0 } | Should -Throw { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ThrottleLimit 65 } | Should -Throw } It 'SampleCount must be positive' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -SampleCount 0 } | Should -Throw } It 'Drive letter must be a single alpha character' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -SourceDriveLetter '12' } | Should -Throw } It 'SourceAccountName is mandatory' { $param = (Get-Command Compare-FileShareIntegrity).Parameters['SourceAccountName'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object { $_.Mandatory | Should -BeTrue } } It 'DestinationAccountName is mandatory' { $param = (Get-Command Compare-FileShareIntegrity).Parameters['DestinationAccountName'] $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object { $_.Mandatory | Should -BeTrue } } It 'SupportsShouldProcess is enabled' { $cmd = Get-Command Compare-FileShareIntegrity $cmdBindingAttr = $cmd.ScriptBlock.Attributes | Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] } $cmdBindingAttr.SupportsShouldProcess | Should -BeTrue } } Context 'Share list failure' { BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } Mock -CommandName 'az' -MockWith { $global:LASTEXITCODE = 1 'AuthorizationFailure' } } It 'Throws when share list retrieval fails' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' } | Should -Throw '*Failed to list*' } } Context 'Empty share list' { BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } Mock -CommandName 'az' -MockWith { $global:LASTEXITCODE = 0 '' } } It 'Throws when no shares are found on source' { { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' } | Should -Throw '*No file shares found*' } } Context 'File comparison with real files via subst drives' { BeforeAll { # Pick two unused drive letters for subst $script:srcLetter = 'V' $script:dstLetter = 'W' # Prepare temp directories with test files $script:srcDir = Join-Path $TestDrive 'src_share' $script:dstDir = Join-Path $TestDrive 'dst_share' New-Item -ItemType Directory -Path $script:srcDir -Force | Out-Null New-Item -ItemType Directory -Path $script:dstDir -Force | Out-Null # identical file Set-Content -Path (Join-Path $script:srcDir 'identical.txt') -Value 'hello world' -NoNewline Set-Content -Path (Join-Path $script:dstDir 'identical.txt') -Value 'hello world' -NoNewline # size mismatch file Set-Content -Path (Join-Path $script:srcDir 'sizemismatch.txt') -Value 'short' -NoNewline Set-Content -Path (Join-Path $script:dstDir 'sizemismatch.txt') -Value 'much longer content' -NoNewline # missing on destination Set-Content -Path (Join-Path $script:srcDir 'missing_dest.txt') -Value 'only on source' -NoNewline # missing on source Set-Content -Path (Join-Path $script:dstDir 'missing_src.txt') -Value 'only on dest' -NoNewline # subdirectory with file New-Item -ItemType Directory -Path (Join-Path $script:srcDir 'sub') -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $script:dstDir 'sub') -Force | Out-Null Set-Content -Path (Join-Path $script:srcDir 'sub\nested.txt') -Value 'nested' -NoNewline Set-Content -Path (Join-Path $script:dstDir 'sub\nested.txt') -Value 'nested' -NoNewline # Create subst drives & subst "${script:srcLetter}:" $script:srcDir 2>$null & subst "${script:dstLetter}:" $script:dstDir 2>$null } AfterAll { & subst "${script:srcLetter}:" /D 2>$null & subst "${script:dstLetter}:" /D 2>$null } BeforeEach { # Override Test-Path mock to return false for our drive letters (bypass "already mounted" check) Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } } It 'Produces CSV report with file comparison results for a named share' { $logDir = Join-Path $TestDrive 'logs_named' New-Item -ItemType Directory -Path $logDir -Force | Out-Null Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -ShareName 'test' ` -SourceDriveLetter $script:srcLetter ` -DestinationDriveLetter $script:dstLetter ` -LogDirectory $logDir # Should have mounted and dismounted Should -Invoke Mount-SmbDrive -Times 2 Should -Invoke Dismount-SmbDrive -Times 2 # Find the CSV report $csvFiles = Get-ChildItem -Path $logDir -Recurse -Filter '*.csv' $csvFiles.Count | Should -BeGreaterOrEqual 1 $csvContent = Import-Csv -Path $csvFiles[0].FullName $statuses = $csvContent.Status | Sort-Object -Unique # Should detect size mismatch and missing files $statuses | Should -Contain 'SizeMismatch' $statuses | Should -Contain 'MissingOnDestination' $statuses | Should -Contain 'MissingOnSource' } It 'Reports WhatIf for deep check instead of actually running it' { $logDir = Join-Path $TestDrive 'logs_whatif' New-Item -ItemType Directory -Path $logDir -Force | Out-Null Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -ShareName 'test' ` -SourceDriveLetter $script:srcLetter ` -DestinationDriveLetter $script:dstLetter ` -LogDirectory $logDir ` -HashAll ` -WhatIf # Should write WhatIf message about deep check Should -Invoke Write-Host -ParameterFilter { $Object -like '*WhatIf*deep-check*' } -Times 1 } It 'Runs with -Partial deep check mode (no parallel needed for 0 candidates when sizes differ)' { # Create a scenario where no same-size candidates exist → deep check has 0 items $logDir = Join-Path $TestDrive 'logs_partial' New-Item -ItemType Directory -Path $logDir -Force | Out-Null # This will run but the Parallel block won't execute for 0 items Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -ShareName 'test' ` -SourceDriveLetter $script:srcLetter ` -DestinationDriveLetter $script:dstLetter ` -LogDirectory $logDir ` -Partial Should -Invoke Mount-SmbDrive -Times 2 } It 'Handles None deep-check mode and shows message' { $logDir = Join-Path $TestDrive 'logs_none' New-Item -ItemType Directory -Path $logDir -Force | Out-Null Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -ShareName 'test' ` -SourceDriveLetter $script:srcLetter ` -DestinationDriveLetter $script:dstLetter ` -LogDirectory $logDir # "No deep-check mode specified" message Should -Invoke Write-Host -ParameterFilter { $Object -like '*No deep-check mode*' } -Times 1 } It 'Handles Sample deep-check mode with count larger than candidates' { $logDir = Join-Path $TestDrive 'logs_sample' New-Item -ItemType Directory -Path $logDir -Force | Out-Null # SampleCount = 1000, but only 2 same-size files exist, so it will take all Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -ShareName 'test' ` -SourceDriveLetter $script:srcLetter ` -DestinationDriveLetter $script:dstLetter ` -LogDirectory $logDir ` -SampleCount 1000 Should -Invoke Mount-SmbDrive -Times 2 } } Context 'Multiple shares - grand summary' { BeforeAll { $script:srcLetter2 = 'V' $script:dstLetter2 = 'W' # Two shares in separate directories $script:srcBase = Join-Path $TestDrive 'multi_src' $script:dstBase = Join-Path $TestDrive 'multi_dst' New-Item -ItemType Directory -Path $script:srcBase -Force | Out-Null New-Item -ItemType Directory -Path $script:dstBase -Force | Out-Null # Files for share1 Set-Content -Path (Join-Path $script:srcBase 'file1.txt') -Value 'content1' -NoNewline Set-Content -Path (Join-Path $script:dstBase 'file1.txt') -Value 'content1' -NoNewline # Create subst (reuse same from previous context if still around) & subst "${script:srcLetter2}:" /D 2>$null & subst "${script:dstLetter2}:" /D 2>$null & subst "${script:srcLetter2}:" $script:srcBase & subst "${script:dstLetter2}:" $script:dstBase } AfterAll { & subst "${script:srcLetter2}:" /D 2>$null & subst "${script:dstLetter2}:" /D 2>$null } BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } Mock -CommandName 'az' -MockWith { $global:LASTEXITCODE = 0 'share1' 'share2' } } It 'Produces grand summary when processing multiple shares' { $logDir = Join-Path $TestDrive 'logs_multi' New-Item -ItemType Directory -Path $logDir -Force | Out-Null Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -SourceDriveLetter $script:srcLetter2 ` -DestinationDriveLetter $script:dstLetter2 ` -LogDirectory $logDir # Grand summary header should be printed Should -Invoke Write-Host -ParameterFilter { $Object -like '*GRAND SUMMARY*' } -Times 1 # Both shares were attempted Should -Invoke Mount-SmbDrive -Times 4 # 2 per share × 2 shares } } Context 'Error handling during share processing' { BeforeAll { $script:srcLetterE = 'V' $script:dstLetterE = 'W' $script:srcDirE = Join-Path $TestDrive 'err_src' New-Item -ItemType Directory -Path $script:srcDirE -Force | Out-Null Set-Content -Path (Join-Path $script:srcDirE 'f.txt') -Value 'x' -NoNewline & subst "${script:srcLetterE}:" /D 2>$null & subst "${script:dstLetterE}:" /D 2>$null & subst "${script:srcLetterE}:" $script:srcDirE # Intentionally do NOT create W: subst → EnumerateFiles will fail } AfterAll { & subst "${script:srcLetterE}:" /D 2>$null & subst "${script:dstLetterE}:" /D 2>$null } BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } } It 'Re-throws when single share processing fails' { { Compare-FileShareIntegrity ` -SourceAccountName 'src' ` -DestinationAccountName 'dst' ` -ShareName 'test' ` -SourceDriveLetter $script:srcLetterE ` -DestinationDriveLetter $script:dstLetterE ` -LogDirectory $TestDrive } | Should -Throw Should -Invoke Dismount-SmbDrive -Times 1 # only source was mounted } } Context 'Cleanup on mount failure' { BeforeEach { Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' } Mock Mount-SmbDrive { throw 'mount error' } -ParameterFilter { $DriveLetter -eq 'Y' } Mock Mount-SmbDrive {} -ParameterFilter { $DriveLetter -eq 'X' } } It 'Dismounts source but not destination when destination mount fails' { try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {} Should -Invoke Dismount-SmbDrive -Times 1 -ParameterFilter { $DriveLetter -eq 'X' } Should -Invoke Dismount-SmbDrive -Times 0 -ParameterFilter { $DriveLetter -eq 'Y' } } } } |