Tests/GitEasy.Releases.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 'release baseline' -Encoding UTF8
        Invoke-TestGit -ArgumentList @('add', '-A') | Out-Null
        Invoke-TestGit -ArgumentList @('commit', '-m', 'release 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 'New-Release' {
    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_NR_$script:Stem")
        $script:TempBare = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_NR_$($script:Stem)_remote.git")
        $script:TempLogs = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_NR_$($script:Stem)_logs")

        New-TestRepositoryWithCommit -Path $script:TempRepo
        New-TestBareRemote          -Path $script:TempBare
        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:TempBare -Recurse -Force -ErrorAction SilentlyContinue
        Remove-Item -LiteralPath $script:TempLogs -Recurse -Force -ErrorAction SilentlyContinue
    }

    It 'creates a release marker at the current saved point' {
        New-Release -Version v1.0.0 -Note 'first release' -NoPush

        $tags = Invoke-TestGit -ArgumentList @('tag', '--list')
        ($tags.Output -join "`n") | Should -Match 'v1\.0\.0'
    }

    It 'records the note as the release annotation' {
        New-Release -Version v1.1.0 -Note 'phase 15 complete' -NoPush

        $msg = Invoke-TestGit -ArgumentList @('tag', '-l', '--format=%(subject)', 'v1.1.0')
        ($msg.Output -join '') | Should -Be 'phase 15 complete'
    }

    It 'returns a structured object describing the release' {
        $result = New-Release -Version v0.2.0 -Note 'beta' -NoPush

        $result | Should -Not -BeNullOrEmpty
        $result.Version | Should -Be 'v0.2.0'
        $result.Note    | Should -Be 'beta'
    }

    It 'publishes the release marker when a remote is configured' {
        Invoke-TestGit -ArgumentList @('remote', 'add', 'origin', $script:TempBare) | Out-Null

        New-Release -Version v3.0.0 -Note 'with publish'

        $remoteRefs = Invoke-TestGit -ArgumentList @('ls-remote', '--tags', $script:TempBare)
        ($remoteRefs.Output -join "`n") | Should -Match 'refs/tags/v3\.0\.0'
    }

    It 'NoPush keeps the release marker local even when a remote is configured' {
        Invoke-TestGit -ArgumentList @('remote', 'add', 'origin', $script:TempBare) | Out-Null

        New-Release -Version v4.0.0 -Note 'local only' -NoPush

        $remoteRefs = Invoke-TestGit -ArgumentList @('ls-remote', '--tags', $script:TempBare)
        @($remoteRefs.Output | Where-Object { $_ -match 'v4\.0\.0' }).Count | Should -Be 0
    }

    It 'refuses to overwrite an existing release without -Force' {
        New-Release -Version v5.0.0 -Note 'first' -NoPush

        $thrown = $null
        try { New-Release -Version v5.0.0 -Note 'duplicate' -NoPush } 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)\bupstream\b'
    }

    It 'overwrites an existing release with -Force' {
        New-Release -Version v6.0.0 -Note 'original note' -NoPush
        New-Release -Version v6.0.0 -Note 'replacement note' -NoPush -Force

        $msg = Invoke-TestGit -ArgumentList @('tag', '-l', '--format=%(subject)', 'v6.0.0')
        ($msg.Output -join '') | Should -Be 'replacement note'
    }

    It 'every invocation writes a log file' {
        New-Release -Version v7.0.0 -Note 'log test' -NoPush

        $logs = @(Get-ChildItem -LiteralPath $script:TempLogs -Filter 'New-Release-*.log' -File)
        $logs.Count -gt 0 | Should -Be $true
        $body = Get-Content -LiteralPath ($logs | Sort-Object LastWriteTime | Select-Object -Last 1).FullName -Raw
        $body | Should -Match 'Outcome: SUCCESS'
    }
}

Describe 'Show-Releases' {
    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_SR_$script:Stem")

        New-TestRepositoryWithCommit -Path $script:TempRepo
        Push-Location -LiteralPath $script:TempRepo
    }

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

    It 'returns empty when there are no releases' {
        $r = @(Show-Releases)
        $r.Count | Should -Be 0
    }

    It 'lists every release with Version, Date, Note' {
        New-Release -Version v1.0.0 -Note 'first'  -NoPush | Out-Null
        New-Release -Version v1.1.0 -Note 'second' -NoPush | Out-Null

        $r = @(Show-Releases)
        $r.Count | Should -Be 2
        $first = $r | Select-Object -First 1
        ($first.PSObject.Properties.Name -contains 'Version') | Should -Be $true
        ($first.PSObject.Properties.Name -contains 'Date')    | Should -Be $true
        ($first.PSObject.Properties.Name -contains 'Note')    | Should -Be $true
    }

    It 'filters by -Pattern' {
        New-Release -Version v1.0.0 -Note 'a'         -NoPush | Out-Null
        New-Release -Version v2.0.0 -Note 'b'         -NoPush | Out-Null
        New-Release -Version beta-1 -Note 'beta tag'  -NoPush | Out-Null

        $r = @(Show-Releases -Pattern 'v*')
        $r.Count | Should -Be 2
    }
}