Tests/GitEasy.Harvest.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 'baseline' -Encoding UTF8
        Invoke-TestGit -ArgumentList @('add', '-A') | Out-Null
        Invoke-TestGit -ArgumentList @('commit', '-m', 'baseline') | Out-Null
    }
    finally {
        Pop-Location
    }
}
}

Describe 'Search-History' {
    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_SH_$script:Stem")

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

        Set-Content -LiteralPath (Join-Path $script:TempRepo 'data.sql') -Value 'CREATE TABLE Foo;' -Encoding UTF8
        Save-Work 'Add Foo table' -NoPush

        Add-Content -LiteralPath (Join-Path $script:TempRepo 'data.sql') -Value 'DROP TABLE Foo;'
        Save-Work 'Drop Foo table' -NoPush

        Add-Content -LiteralPath (Join-Path $script:TempRepo 'data.sql') -Value 'CREATE TABLE Bar;'
        Save-Work 'Add Bar table' -NoPush
    }

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

    It 'finds saved points that touched a string' {
        $hits = @(Search-History -Pattern 'DROP TABLE Foo')
        $hits.Count -gt 0 | Should -Be $true
    }

    It 'returns objects with Id, Date, Author, Message' {
        $hits = @(Search-History -Pattern 'CREATE TABLE Foo')
        $first = $hits | Select-Object -First 1
        ($first.PSObject.Properties.Name -contains 'Id')      | Should -Be $true
        ($first.PSObject.Properties.Name -contains 'Date')    | Should -Be $true
        ($first.PSObject.Properties.Name -contains 'Author')  | Should -Be $true
        ($first.PSObject.Properties.Name -contains 'Message') | Should -Be $true
    }

    It '-Patch adds a Change property with the diff text' {
        $hits = @(Search-History -Pattern 'DROP TABLE Foo' -Patch)
        $first = $hits | Select-Object -First 1
        ($first.PSObject.Properties.Name -contains 'Change') | Should -Be $true
        $first.Change | Should -Match 'DROP TABLE Foo'
    }

    It 'returns empty when no saved point touched the pattern' {
        $hits = @(Search-History -Pattern 'NonExistentString12345')
        $hits.Count | Should -Be 0
    }
}

Describe 'Show-History -Graph' {
    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_Graph_$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 'does not throw when -Graph is supplied' {
        $thrown = $null
        try { Show-History -Graph } catch { $thrown = $_ }
        $thrown | Should -BeNullOrEmpty
    }
}

Describe 'Save-Work -BumpVersion' {
    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_BV_$script:Stem")

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

        Set-Content -LiteralPath (Join-Path $script:TempRepo 'TestModule.psd1') -Encoding UTF8 -Value @"
@{
    RootModule = 'TestModule.psm1'
    ModuleVersion = '1.2.3'
    GUID = '00000000-0000-0000-0000-000000000099'
    Author = 'Test'
}
"@

        Set-Content -LiteralPath (Join-Path $script:TempRepo 'TestModule.psm1') -Value 'function Test-Func {}' -Encoding UTF8
        Invoke-TestGit -ArgumentList @('add', '-A') | Out-Null
        Invoke-TestGit -ArgumentList @('commit', '-m', 'add test module manifest') | Out-Null
    }

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

    It 'bumps the Build segment by default' {
        Set-Content -LiteralPath (Join-Path $script:TempRepo 'change.txt') -Value 'change' -Encoding UTF8
        Save-Work 'change' -NoPush -BumpVersion

        $manifest = Import-PowerShellDataFile -LiteralPath (Join-Path $script:TempRepo 'TestModule.psd1')
        $manifest.ModuleVersion | Should -Be '1.2.4'
    }

    It 'bumps the Minor segment when -BumpKind Minor is supplied' {
        Set-Content -LiteralPath (Join-Path $script:TempRepo 'change.txt') -Value 'change' -Encoding UTF8
        Save-Work 'change' -NoPush -BumpVersion -BumpKind Minor

        $manifest = Import-PowerShellDataFile -LiteralPath (Join-Path $script:TempRepo 'TestModule.psd1')
        $manifest.ModuleVersion | Should -Be '1.3.0'
    }

    It 'bumps the Major segment when -BumpKind Major is supplied' {
        Set-Content -LiteralPath (Join-Path $script:TempRepo 'change.txt') -Value 'change' -Encoding UTF8
        Save-Work 'change' -NoPush -BumpVersion -BumpKind Major

        $manifest = Import-PowerShellDataFile -LiteralPath (Join-Path $script:TempRepo 'TestModule.psd1')
        $manifest.ModuleVersion | Should -Be '2.0.0'
    }

    It 'prefixes the saved-point message with the new version' {
        Set-Content -LiteralPath (Join-Path $script:TempRepo 'change.txt') -Value 'change' -Encoding UTF8
        Save-Work 'fix the thing' -NoPush -BumpVersion -BumpKind Minor

        $log = Invoke-TestGit -ArgumentList @('log', '-1', '--pretty=%s')
        ($log.Output -join '') | Should -Match '^\[v1\.3\.0\] fix the thing$'
    }

    It 'finds and bumps a manifest in the conventional ModuleName\ModuleName.psd1 nested layout' {
        $nested = Join-Path $script:TempRepo 'NestedModule'
        New-Item -Path $nested -ItemType Directory -Force | Out-Null

        Set-Content -LiteralPath (Join-Path $nested 'NestedModule.psd1') -Encoding UTF8 -Value @"
@{
    RootModule = 'NestedModule.psm1'
    ModuleVersion = '2.5.7'
    GUID = '00000000-0000-0000-0000-000000000077'
    Author = 'Test'
}
"@

        Set-Content -LiteralPath (Join-Path $nested 'NestedModule.psm1') -Value 'function NestedFn {}' -Encoding UTF8
        Invoke-TestGit -ArgumentList @('add', '-A') | Out-Null
        Invoke-TestGit -ArgumentList @('commit', '-m', 'add nested module') | Out-Null

        Set-Content -LiteralPath (Join-Path $script:TempRepo 'change.txt') -Value 'something' -Encoding UTF8
        Save-Work 'nested bump' -NoPush -BumpVersion -BumpKind Minor

        # The nested manifest should be bumped (1.2.3 -> 2.6.0 since both manifests exist; conventional preference picks the nested one)
        $nestedManifest = Import-PowerShellDataFile -LiteralPath (Join-Path $nested 'NestedModule.psd1')
        $nestedManifest.ModuleVersion | Should -Be '2.6.0'
    }
}

Describe 'Set-Vault -WriteIgnoreList' {
    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
        }
    }

    BeforeEach {
        $script:Stem     = [guid]::NewGuid().ToString('N').Substring(0, 8)
        $script:TempRepo = Join-Path ([System.IO.Path]::GetTempPath()) ("GitEasy_WIL_$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 'writes a starter .gitignore when one does not exist' {
        Set-Vault -WriteIgnoreList | Out-Null

        $ignorePath = Join-Path $script:TempRepo '.gitignore'
        Test-Path -LiteralPath $ignorePath | Should -Be $true

        $body = Get-Content -LiteralPath $ignorePath -Raw
        $body | Should -Match '\*\.bak'
        $body | Should -Match 'bin/'
        $body | Should -Match 'obj/'
    }

    It 'preserves existing .gitignore content and only appends missing patterns' {
        $ignorePath = Join-Path $script:TempRepo '.gitignore'
        Set-Content -LiteralPath $ignorePath -Value 'my-custom-pattern' -Encoding UTF8

        Set-Vault -WriteIgnoreList | Out-Null

        $body = Get-Content -LiteralPath $ignorePath -Raw
        $body | Should -Match 'my-custom-pattern'
        $body | Should -Match '\*\.bak'
    }

    It 'reports how many patterns were added' {
        $result = Set-Vault -WriteIgnoreList

        $result.IgnoreList | Should -Not -BeNullOrEmpty
        ($result.IgnoreList.PSObject.Properties.Name -contains 'Added') | Should -Be $true
    }
}