Tests/GitEasy.AuthCommands.Tests.ps1

BeforeAll {
$ProjectRoot = Split-Path -Parent $PSScriptRoot
$ModulePath  = Join-Path $ProjectRoot 'GitEasy.psd1'

function Invoke-TestGit {
    <#
    .DESCRIPTION
    Test helper. Runs git with the given arguments and returns exit code and output.
    Steps:
    1. Run git capturing combined output.
    2. Throw on non-zero exit unless -AllowFailure is set.
    3. Return a result object with the exit code and output array.
    #>

    param(
        [Parameter(Mandatory)]
        [string[]]$ArgumentList,

        [switch]$AllowFailure
    )

    $oldPreference = $ErrorActionPreference

    try {
        $ErrorActionPreference = 'Continue'
        $output = & git @ArgumentList 2>&1
        $exitCode = $LASTEXITCODE
    }
    finally {
        $ErrorActionPreference = $oldPreference
    }

    if (($exitCode -ne 0) -and (-not $AllowFailure)) {
        throw "Git failed: git $($ArgumentList -join ' ')`n$($output -join [Environment]::NewLine)"
    }

    return [PSCustomObject]@{
        ExitCode = $exitCode
        Output   = @($output)
    }
}

function New-TestRepositoryWithCommit {
    <#
    .DESCRIPTION
    Test helper. Creates a git repository with one saved point for test isolation.
    Steps:
    1. Create the target directory.
    2. Initialize a new repository and set test-safe user name and email.
    3. Write a README file, stage it, and create the first saved point.
    #>

    param([Parameter(Mandatory)] [string]$Path)

    New-Item -Path $Path -ItemType Directory -Force | Out-Null
    Push-Location -LiteralPath $Path

    try {
        Invoke-TestGit -ArgumentList @('init') | Out-Null
        Invoke-TestGit -ArgumentList @('config', 'user.name', 'GitEasy Pester') | Out-Null
        Invoke-TestGit -ArgumentList @('config', 'user.email', 'giteasy-pester@example.invalid') | Out-Null
        Set-Content -LiteralPath (Join-Path $Path 'README.md') -Value 'auth baseline' -Encoding UTF8
        Invoke-TestGit -ArgumentList @('add', '-A') | Out-Null
        Invoke-TestGit -ArgumentList @('commit', '-m', 'auth baseline') | Out-Null
    }
    finally {
        Pop-Location
    }
}

function New-TestBareRemote {
    <#
    .DESCRIPTION
    Test helper. Creates a bare repository to serve as a published location in tests.
    Steps:
    1. Create the target directory.
    2. Initialize a bare repository.
    #>

    param([Parameter(Mandatory)] [string]$Path)

    New-Item -Path $Path -ItemType Directory -Force | Out-Null
    Push-Location -LiteralPath $Path

    try {
        Invoke-TestGit -ArgumentList @('init', '--bare') | Out-Null
    }
    finally {
        Pop-Location
    }
}
}

Describe 'Test-Login' {
    BeforeAll {
        Remove-Module GitEasy -Force -ErrorAction SilentlyContinue
        Import-Module $ModulePath -Force
    }

    BeforeEach {
        $script:Stem     = [guid]::NewGuid().ToString('N').Substring(0, 8)
        $script:TempRepo = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_TL_$script:Stem")
        $script:TempBare = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_TL_$($script:Stem)_remote.git")

        New-TestRepositoryWithCommit -Path $script:TempRepo
        New-TestBareRemote          -Path $script:TempBare

        Push-Location -LiteralPath $script:TempRepo
    }

    AfterEach {
        Pop-Location
        Remove-Item -LiteralPath $script:TempRepo -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $script:TempBare -Recurse -Force -ErrorAction SilentlyContinue
    }

    It 'reports Passed=false when no remote is configured' {
        $result = Test-Login
        $result.Passed | Should -Be $false
    }

    It 'returns an object with the expected shape' {
        $result = Test-Login

        ($result.PSObject.Properties.Name -contains 'Repository') | Should -Be $true
        ($result.PSObject.Properties.Name -contains 'Branch')     | Should -Be $true
        ($result.PSObject.Properties.Name -contains 'Remote')     | Should -Be $true
        ($result.PSObject.Properties.Name -contains 'Provider')   | Should -Be $true
        ($result.PSObject.Properties.Name -contains 'Url')        | Should -Be $true
        ($result.PSObject.Properties.Name -contains 'Passed')     | Should -Be $true
        ($result.PSObject.Properties.Name -contains 'Message')    | Should -Be $true
    }

    It 'reports Passed=true when the remote is reachable' {
        Invoke-TestGit -ArgumentList @('remote', 'add', 'origin', $script:TempBare) | Out-Null

        $result = Test-Login
        $result.Passed | Should -Be $true
    }

    It 'reports Passed=false when the remote URL is bogus' {
        $bogus = 'file:///' + (Join-Path ([System.IO.Path]::GetTempPath()) ('does-not-exist-' + [guid]::NewGuid().ToString('N') + '.git')).Replace('\','/')
        Invoke-TestGit -ArgumentList @('remote', 'add', 'origin', $bogus) | Out-Null

        $result = Test-Login
        $result.Passed | Should -Be $false
    }
}

Describe 'Set-Vault' {
    BeforeAll {
        Remove-Module GitEasy -Force -ErrorAction SilentlyContinue
        Import-Module $ModulePath -Force

        $script:OriginalHelper = $null
        $r = Invoke-TestGit -ArgumentList @('config', '--global', '--get', 'credential.helper') -AllowFailure
        if ($r.ExitCode -eq 0) {
            $script:OriginalHelper = $r.Output | Select-Object -First 1
        }
    }

    AfterAll {
        if ($null -ne $script:OriginalHelper) {
            Invoke-TestGit -ArgumentList @('config', '--global', 'credential.helper', $script:OriginalHelper) -AllowFailure | Out-Null
        }
        else {
            Invoke-TestGit -ArgumentList @('config', '--global', '--unset', 'credential.helper') -AllowFailure | Out-Null
        }
    }

    It 'sets the global credential helper' {
        Set-Vault -Helper 'manager' | Out-Null

        $r = Invoke-TestGit -ArgumentList @('config', '--global', '--get', 'credential.helper') -AllowFailure
        $r.ExitCode | Should -Be 0
        ($r.Output | Select-Object -First 1) | Should -Be 'manager'
    }

    It 'returns an object describing the configured helper' {
        $result = Set-Vault -Helper 'wincred'

        $result | Should -Not -BeNullOrEmpty
        $result.CredentialHelper | Should -Be 'wincred'
    }

    It 'rejects helper values outside the validate set' {
        $caught = $null
        try { Set-Vault -Helper 'bogus' } catch { $caught = $_ }
        $caught | Should -Not -BeNullOrEmpty
    }
}

Describe 'Reset-Login' {
    BeforeAll {
        Remove-Module GitEasy -Force -ErrorAction SilentlyContinue
        Import-Module $ModulePath -Force
    }

    BeforeEach {
        $script:Stem     = [guid]::NewGuid().ToString('N').Substring(0, 8)
        $script:TempRepo = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_RL_$script:Stem")
        $script:TempLogs = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_RL_$($script:Stem)_logs")

        New-TestRepositoryWithCommit -Path $script:TempRepo
        New-Item -Path $script:TempLogs -ItemType Directory -Force | Out-Null

        $env:GITEASY_LOG_PATH = $script:TempLogs

        Push-Location -LiteralPath $script:TempRepo
    }

    AfterEach {
        Pop-Location
        Remove-Item Env:\GITEASY_LOG_PATH -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $script:TempRepo -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $script:TempLogs -Recurse -Force -ErrorAction SilentlyContinue
    }

    It 'fails plainly when there is no remote configured' {
        $thrown = $null
        try { Reset-Login } catch { $thrown = $_ }

        $thrown | Should -Not -BeNullOrEmpty
        $thrown.Exception.Message | Should -Match '(?i)Details:'

        $userMessage = $thrown.Exception.Message -replace '(?ms)Details:.*$',''
        $userMessage | Should -Not -Match '(?i)\bgit\b'
    }

    It 'refuses non-HTTPS remotes plainly' {
        Invoke-TestGit -ArgumentList @('remote', 'add', 'origin', 'git@example.com:foo/bar.git') | Out-Null

        $thrown = $null
        try { Reset-Login } catch { $thrown = $_ }

        $thrown | Should -Not -BeNullOrEmpty

        $userMessage = $thrown.Exception.Message -replace '(?ms)Details:.*$',''
        $userMessage | Should -Not -Match '(?i)\bupstream\b'
        $userMessage | Should -Not -Match '(?i)\bHEAD\b'
        $userMessage | Should -Not -Match '(?i)\brefspec\b'
    }

    It 'every invocation writes a log file' {
        # Reset-Login is expected to fail here (no remote configured in the
        # test repo). Assert the throw happened first, so the log-file check
        # is not a silent pass if Reset-Login ever succeeds unexpectedly.
        $thrown = $null
        try { Reset-Login } catch { $thrown = $_ }

        $thrown | Should -Not -BeNullOrEmpty

        $logs = @(Get-ChildItem -LiteralPath $script:TempLogs -Filter 'Reset-Login-*.log' -File)
        $logs.Count -gt 0 | Should -Be $true
    }
}