Tests/GitEasy.GetVaultStatus.Tests.ps1
|
BeforeAll { $ProjectRoot = Split-Path -Parent $PSScriptRoot $ModulePath = Join-Path $ProjectRoot 'GitEasy.psd1' # --------------------------------------------------------------------------- # Invoke-TestGit — thin wrapper used only to set up git state in temp repos. # Mirrors the pattern from SaveWork.Tests.ps1. # --------------------------------------------------------------------------- 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) } } # --------------------------------------------------------------------------- # Read the real global credential.helper value once so we can restore it. # Tests that mutate this key must restore it in AfterEach. # --------------------------------------------------------------------------- function Get-GlobalCredentialHelper { <# .DESCRIPTION Test helper. Reads the global credential storage configuration value. Steps: 1. Query the global credential.helper setting. 2. Return the value, or $null if not set. #> $r = Invoke-TestGit -ArgumentList @('config', '--global', '--get', 'credential.helper') -AllowFailure if ($r.ExitCode -eq 0) { return ($r.Output | Select-Object -First 1) } return $null } function Set-GlobalCredentialHelper { <# .DESCRIPTION Test helper. Sets the global credential storage configuration to the given value. Steps: 1. Write the given value to the global credential.helper setting. #> param([string]$Value) Invoke-TestGit -ArgumentList @('config', '--global', 'credential.helper', $Value) | Out-Null } function Remove-GlobalCredentialHelper { <# .DESCRIPTION Test helper. Removes the global credential storage configuration. Steps: 1. Unset the global credential.helper setting (best effort; failure is not fatal). #> Invoke-TestGit -ArgumentList @('config', '--global', '--unset', 'credential.helper') -AllowFailure | Out-Null } } Describe 'Get-VaultStatus — contract' { BeforeAll { Remove-Module GitEasy -Force -ErrorAction SilentlyContinue Import-Module $ModulePath -Force } # ----------------------------------------------------------------------- # Return-shape tests: do not touch the real git global config. # We mock Invoke-GEGit so the tests are hermetic and fast. # ----------------------------------------------------------------------- Context 'Return shape' { It 'returns a PSCustomObject, not $null' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus $result | Should -Not -BeNullOrEmpty } It 'result has a CredentialHelper property' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus ($result | Get-Member -Name CredentialHelper) | Should -Not -BeNullOrEmpty } It 'result has a Configured property' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus ($result | Get-Member -Name Configured) | Should -Not -BeNullOrEmpty } It 'result has exactly two properties — no extra fields leak out' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus $propNames = ($result | Get-Member -MemberType NoteProperty).Name | Sort-Object $propNames.Count | Should -Be 2 } } # ----------------------------------------------------------------------- # Configured-path tests: helper is present and exit code is 0. # ----------------------------------------------------------------------- Context 'Configured path — credential.helper is set' { It 'Configured is $true when a helper name is returned' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus $result.Configured | Should -Be $true } It 'CredentialHelper echoes back the value git config returned' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('osxkeychain'); Stderr = @() } } $result = Get-VaultStatus $result.CredentialHelper | Should -Be 'osxkeychain' } It 'only the first line of git output is used when multiple lines are returned' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core', 'store'); Stderr = @() } } $result = Get-VaultStatus $result.CredentialHelper | Should -Be 'manager-core' } } # ----------------------------------------------------------------------- # Unconfigured-path tests: git config exits non-zero / returns nothing. # ----------------------------------------------------------------------- Context 'Unconfigured path — credential.helper is absent' { It 'Configured is $false when git config exits non-zero (key not set)' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 1; Output = @(); Stderr = @() } } $result = Get-VaultStatus $result.Configured | Should -Be $false } It 'CredentialHelper is $null or empty when git config exits non-zero' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 1; Output = @(); Stderr = @() } } $result = Get-VaultStatus $result.CredentialHelper | Should -BeNullOrEmpty } It 'Configured is $false when exit code is 0 but output is whitespace only' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @(' '); Stderr = @() } } $result = Get-VaultStatus $result.Configured | Should -Be $false } } # ----------------------------------------------------------------------- # "Never returns secrets" contract (per the function's .NOTES). # ----------------------------------------------------------------------- Context 'Safe-to-share output — no secret values exposed' { It 'CredentialHelper contains the helper name, not a password or token' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus # The contract: only the storage name (a short word or path) is returned, # never a string that looks like a credential (long base64, token prefix, etc). $result.CredentialHelper | Should -Match '^[\w\-\.\/\\]+$' } It 'result does not contain a property named Password, Token, Secret, or Key' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } $result = Get-VaultStatus $propNames = ($result | Get-Member -MemberType NoteProperty).Name @($propNames | Where-Object { $_ -match '(?i)(password|token|secret|key)' }).Count | Should -Be 0 } } # ----------------------------------------------------------------------- # git-not-installed path: Test-GEGitInstalled throws, function propagates. # ----------------------------------------------------------------------- Context 'git not installed' { It 'throws when Test-GEGitInstalled reports git is missing' { Mock -ModuleName GitEasy Test-GEGitInstalled { throw 'Git was not found in PATH.' } $thrown = $null try { Get-VaultStatus } catch { $thrown = $_ } $thrown | Should -Not -BeNullOrEmpty } It 'error message does not contain internal git jargon when git is missing' { Mock -ModuleName GitEasy Test-GEGitInstalled { throw 'Git was not found in PATH.' } $thrown = $null try { Get-VaultStatus } catch { $thrown = $_ } # The word "git" in the error is allowed here (it names the dependency), # but internal flags, refspecs, plumbing terms must not appear. $thrown.Exception.Message | Should -Not -Match '(?i)\brefspec\b' $thrown.Exception.Message | Should -Not -Match '(?i)\bHEAD\b' } } # ----------------------------------------------------------------------- # Invoke-GEGit call contract: correct arguments forwarded. # ----------------------------------------------------------------------- Context 'Invoke-GEGit call contract' { It 'calls Invoke-GEGit with the credential.helper query arguments' { # Pester 3 + module-scoped mock: capturing state via $script: from # inside a -ModuleName mock writes to the module's scope, not the # test's. Assert-MockCalled -ParameterFilter is the idiomatic # way to inspect what arguments the mock saw. Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 0; Output = @('manager-core'); Stderr = @() } } Get-VaultStatus | Out-Null Assert-MockCalled Invoke-GEGit -ModuleName GitEasy -Times 1 -ParameterFilter { ($ArgumentList -join ' ') -match 'credential\.helper' } } It 'passes -AllowFailure so a missing key does not terminate the function' { Mock -ModuleName GitEasy Test-GEGitInstalled { $true } Mock -ModuleName GitEasy Invoke-GEGit { [PSCustomObject]@{ ExitCode = 1; Output = @(); Stderr = @() } } Get-VaultStatus | Out-Null Assert-MockCalled Invoke-GEGit -ModuleName GitEasy -Times 1 -ParameterFilter { $AllowFailure -eq $true } } } # ----------------------------------------------------------------------- # Real-git integration: exercise against the actual global config. # Isolated by saving and restoring the pre-test helper value. # ----------------------------------------------------------------------- Context 'Real-git integration — live global config' { BeforeEach { $script:OriginalHelper = Get-GlobalCredentialHelper } AfterEach { if ($null -ne $script:OriginalHelper -and $script:OriginalHelper -ne '') { Set-GlobalCredentialHelper -Value $script:OriginalHelper } else { Remove-GlobalCredentialHelper } } It 'Configured is $true after setting a real credential.helper value' { Set-GlobalCredentialHelper -Value 'store' $result = Get-VaultStatus $result.Configured | Should -Be $true $result.CredentialHelper | Should -Be 'store' } It 'Configured is $false after unsetting credential.helper' { Remove-GlobalCredentialHelper $result = Get-VaultStatus $result.Configured | Should -Be $false } It 'CredentialHelper value is never $null when Configured is $true' { Set-GlobalCredentialHelper -Value 'manager' $result = Get-VaultStatus if ($result.Configured) { $result.CredentialHelper | Should -Not -BeNullOrEmpty } else { # helper was already absent before we could set it; assert # the contract we DO know in this branch instead of $true|=$true. $result.Configured | Should -Be $false } } } } |