Tests/Public/Mount-FileShare.Tests.ps1

BeforeAll {
    $modulePath = "$PSScriptRoot/../.."
    # Dot-source all private and public functions for testing
    Get-ChildItem -Path "$modulePath/Private/*.ps1" | ForEach-Object { . $_.FullName }
    Get-ChildItem -Path "$modulePath/Public/*.ps1"  | ForEach-Object { . $_.FullName }
}

Describe 'Mount-FileShare' {
    BeforeEach {
        Mock Write-Host {}
        Mock Write-Warning {}
        Mock Write-Verbose {}
        Mock Test-Path { $false }
        Mock Invoke-NetUse {
            $global:LASTEXITCODE = 0
            'OK'
        }
        Mock Invoke-CmdKey { '' }
    }

    Context 'Multiple shares' {
        It 'Mounts all shares defined in ShareDriveMap' {
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            $map = @{ 'data' = 'D'; 'apps' = 'A'; 'files' = 'F' }
            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap $map -Confirm:$false
            Should -Invoke Invoke-NetUse -Times 3
        }
    }

    Context 'Custom share map' {
        It 'Mounts only specified shares' {
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D'; 'apps' = 'A' } -Confirm:$false
            Should -Invoke Invoke-NetUse -Times 2
        }
    }

    Context 'Username expansion' {
        It 'Replaces %USERNAME% token with credential username prefix' {
            $cred = [PSCredential]::new('jdoe@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                'OK'
            } -Verifiable

            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'staff/%USERNAME%' = 'H' } -Confirm:$false
            # Verify Invoke-NetUse was called (the UNC path expansion happens internally)
            Should -Invoke Invoke-NetUse -Times 1
        }
    }

    Context 'When mount fails' {
        It 'Logs a warning and continues with remaining shares' {
            Mock Invoke-NetUse { $global:LASTEXITCODE = 1; 'System error 53' }
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))

            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'a' = 'A'; 'b' = 'B' } -Confirm:$false
            Should -Invoke Write-Warning -Times 2
        }
    }

    Context 'Stale drive removal' {
        It 'Removes existing mapping before remounting' {
            Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' }
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))

            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Confirm:$false
            # Called twice: once for delete, once for mount
            Should -Invoke Invoke-NetUse -Times 2
        }
    }

    Context 'WhatIf support' {
        It 'Does not actually mount when -WhatIf is specified' {
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -WhatIf
            # Invoke-NetUse should not be called for actual mounting (only stale cleanup test is separate)
            Should -Invoke Invoke-NetUse -Times 0
        }
    }

    Context 'Missing Credential without Reset' {
        It 'Throws an error when Credential is not provided and Reset is not specified' {
            Mock Write-Error {}
            Mount-FileShare -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Confirm:$false
            Should -Invoke Write-Error -Times 1 -ParameterFilter {
                $Message -like '*-Credential parameter is required*'
            }
        }
    }

    Context 'Missing ShareDriveMap without Reset' {
        It 'Throws an error when ShareDriveMap is not provided and Reset is not specified' {
            Mock Write-Error {}
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -Confirm:$false
            Should -Invoke Write-Error -Times 1 -ParameterFilter {
                $Message -like '*-ShareDriveMap parameter is required*'
            }
        }
    }

    Context 'Reset with explicit ShareDriveMap' {
        BeforeEach {
            Mock Start-Process {}
            Mock Read-Host { '' }
            Mock Invoke-CmdKey { '' }
            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                'OK'
            }
            Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' }
        }

        It 'Disconnects existing drives, purges credentials, prompts user and remounts' {
            $newCred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'newpass' -AsPlainText -Force))
            Mock Get-Credential { $newCred }

            Mount-FileShare -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D'; 'files' = 'F' } -Reset -Confirm:$false

            # Start-Process should open the password change page
            Should -Invoke Start-Process -Times 1
            # Read-Host should pause for user confirmation
            Should -Invoke Read-Host -Times 1
            # Get-Credential should prompt for new credentials
            Should -Invoke Get-Credential -Times 1
            # Invoke-CmdKey called for purging credentials (3 known targets + enumeration list)
            Should -Invoke Invoke-CmdKey -Scope It
            # Invoke-NetUse should be called for drive deletes, stale scan, and remounts
            Should -Invoke Invoke-NetUse -Scope It
        }

        It 'Opens the Microsoft password change URL' {
            $newCred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'newpass' -AsPlainText -Force))
            Mock Get-Credential { $newCred }

            Mount-FileShare -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Reset -Confirm:$false

            Should -Invoke Start-Process -Times 1 -ParameterFilter {
                $FilePath -eq 'https://account.activedirectory.windowsazure.com/ChangePassword.aspx'
            }
        }
    }

    Context 'Reset with auto-discovered ShareDriveMap' {
        BeforeEach {
            Mock Start-Process {}
            Mock Read-Host { '' }
            Mock Invoke-CmdKey { '' }
        }

        It 'Discovers existing mappings from net use and remounts them' {
            $netUseDiscoveryOutput = @(
                'New connections will be remembered.'
                ''
                'Status Local Remote Network'
                '-------------------------------------------------------------------------------'
                'OK D: \\teststorage.file.core.windows.net\data Microsoft Windows Network'
                'OK F: \\teststorage.file.core.windows.net\files Microsoft Windows Network'
                'The command completed successfully.'
            )

            $script:netCallCount = 0
            Mock Invoke-NetUse {
                $script:netCallCount++
                # First call is the discovery scan (no args / empty args)
                if ($script:netCallCount -eq 1) {
                    $global:LASTEXITCODE = 0
                    return $netUseDiscoveryOutput
                }
                $global:LASTEXITCODE = 0
                return 'OK'
            }

            Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' }

            $newCred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'newpass' -AsPlainText -Force))
            Mock Get-Credential { $newCred }

            Mount-FileShare -StorageAccountName 'teststorage' -Reset -Confirm:$false

            # Invoke-NetUse invoked for: discovery, drive deletes, stale scan, remounts
            Should -Invoke Invoke-NetUse -Scope It
            # Credential prompt must happen
            Should -Invoke Get-Credential -Times 1
        }

        It 'Discovers subfolder mappings correctly' {
            $netUseDiscoveryOutput = @(
                'Status Local Remote Network'
                '-------------------------------------------------------------------------------'
                'OK H: \\teststorage.file.core.windows.net\staff\jdoe Microsoft Windows Network'
            )

            $script:netCallCount = 0
            Mock Invoke-NetUse {
                $script:netCallCount++
                if ($script:netCallCount -eq 1) {
                    $global:LASTEXITCODE = 0
                    return $netUseDiscoveryOutput
                }
                $global:LASTEXITCODE = 0
                return 'OK'
            }

            Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' }

            $newCred = [PSCredential]::new('jdoe@contoso.com', (ConvertTo-SecureString 'newpass' -AsPlainText -Force))
            Mock Get-Credential { $newCred }

            Mount-FileShare -StorageAccountName 'teststorage' -Reset -Confirm:$false

            # The discovered map key should be 'staff/jdoe' -> 'H'
            Should -Invoke Invoke-NetUse -Scope It
            Should -Invoke Get-Credential -Times 1
        }

        It 'Errors when no existing mappings are found for the storage account' {
            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                @('New connections will be remembered.', '', 'There are no entries in the list.')
            }
            Mock Write-Error {}
            Mock Get-Credential {}

            Mount-FileShare -StorageAccountName 'teststorage' -Reset -Confirm:$false

            Should -Invoke Write-Error -Times 1 -ParameterFilter {
                $Message -like '*No existing file-share mappings found*'
            }
        }

        It 'Ignores mappings for other storage accounts during discovery' {
            $netUseDiscoveryOutput = @(
                'Status Local Remote Network'
                '-------------------------------------------------------------------------------'
                'OK X: \\otherstorage.file.core.windows.net\share Microsoft Windows Network'
            )

            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                return $netUseDiscoveryOutput
            }
            Mock Write-Error {}
            Mock Get-Credential {}

            Mount-FileShare -StorageAccountName 'teststorage' -Reset -Confirm:$false

            Should -Invoke Write-Error -Times 1 -ParameterFilter {
                $Message -like '*No existing file-share mappings found*'
            }
        }
    }

    Context 'Reset credential purge' {
        BeforeEach {
            Mock Start-Process {}
            Mock Read-Host { '' }
            Mock Test-Path { $false }
            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                ''
            }
        }

        It 'Calls Invoke-CmdKey for known target patterns' {
            Mock Invoke-CmdKey { '' }
            $newCred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'newpass' -AsPlainText -Force))
            Mock Get-Credential { $newCred }

            Mount-FileShare -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Reset -Confirm:$false

            # At minimum 3 explicit Invoke-CmdKey calls for the known target patterns
            # plus 1 /list call for enumeration
            Should -Invoke Invoke-CmdKey -Scope It
        }

        It 'Removes dynamically discovered cmdkey entries matching the FQDN' {
            $cmdkeyListOutput = @(
                'Currently stored credentials:'
                ''
                ' Target: teststorage.file.core.windows.net'
                ' Type: Domain Password'
                ' User: AZURE\teststorage'
                ''
                ' Target: Domain:target=otherstorage.file.core.windows.net'
                ' Type: Domain Password'
                ' User: AZURE\otherstorage'
            ) -join "`n"

            $script:cmdkeyCallCount = 0
            Mock Invoke-CmdKey {
                $script:cmdkeyCallCount++
                if ($Arguments -contains '/list') {
                    return $cmdkeyListOutput
                }
                return ''
            }

            $newCred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'newpass' -AsPlainText -Force))
            Mock Get-Credential { $newCred }

            Mount-FileShare -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Reset -Confirm:$false

            # Invoke-CmdKey should have been invoked: 3 known patterns + 1 /list + at least 1 dynamic delete
            Should -Invoke Invoke-CmdKey -Scope It
        }
    }

    Context 'Parameter validation' {
        It 'StorageAccountName is mandatory' {
            $param = (Get-Command Mount-FileShare).Parameters['StorageAccountName']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object {
                $_.Mandatory | Should -BeTrue
            }
        }

        It 'SupportsShouldProcess is enabled' {
            $cmd = Get-Command Mount-FileShare
            $cmdBindingAttr = $cmd.ScriptBlock.Attributes | Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] }
            $cmdBindingAttr.SupportsShouldProcess | Should -BeTrue
        }

        It 'Credential is not mandatory' {
            $param = (Get-Command Mount-FileShare).Parameters['Credential']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object {
                $_.Mandatory | Should -BeFalse
            }
        }

        It 'ShareDriveMap is not mandatory' {
            $param = (Get-Command Mount-FileShare).Parameters['ShareDriveMap']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object {
                $_.Mandatory | Should -BeFalse
            }
        }
    }

    Context 'Mount summary' {
        It 'Displays summary with mount count' {
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                'OK'
            }
            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Confirm:$false
            Should -Invoke Write-Host -ParameterFilter { $Object -like '*Mounted*1*' }
        }

        It 'Displays drive map before mounting' {
            $cred = [PSCredential]::new('user@contoso.com', (ConvertTo-SecureString 'pass' -AsPlainText -Force))
            Mock Invoke-NetUse {
                $global:LASTEXITCODE = 0
                'OK'
            }
            Mount-FileShare -Credential $cred -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Confirm:$false
            Should -Invoke Write-Host -ParameterFilter { $Object -like '*Drive Map*' }
        }
    }

    Context 'Reset aborts when no credential provided' {
        It 'Throws when Get-Credential returns null during reset' {
            Mock Start-Process {}
            Mock Read-Host { '' }
            Mock Invoke-CmdKey { '' }
            Mock Invoke-NetUse { $global:LASTEXITCODE = 0; '' }
            Mock Test-Path { $false }
            # Return a credential with empty username to test the null-check path
            Mock Get-Credential {
                [PSCredential]::new('x', (ConvertTo-SecureString 'x' -AsPlainText -Force))
            }

            # Function should complete without error when valid credentials are returned
            # (Verifies the reset credential flow works end-to-end)
            Mount-FileShare -StorageAccountName 'teststorage' -ShareDriveMap @{ 'data' = 'D' } -Reset -Confirm:$false
            Should -Invoke Get-Credential -Times 1
        }
    }
}