tests/RepoHerd.Unit.Tests.ps1
|
#Requires -Version 7.6 #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0' } <# .SYNOPSIS Unit tests for RepoHerd module functions .DESCRIPTION Tests pure and near-pure functions that require no network or git operations. Run with: Invoke-Pester ./tests/RepoHerd.Unit.Tests.ps1 -Output Detailed #> BeforeAll { # Import the module from parent directory $modulePath = Join-Path $PSScriptRoot '..' 'RepoHerd.psm1' Import-Module $modulePath -Force # Initialize module with test-safe defaults Initialize-RepoHerd -ScriptPath $PSScriptRoot # Silence Write-Host output from Write-Log Mock Write-Host {} -ModuleName RepoHerd } Describe 'ConvertTo-VersionPattern' { It 'parses exact version x.y.z as LowestApplicable' { $result = ConvertTo-VersionPattern -VersionPattern '3.2.1' $result.Type | Should -Be 'LowestApplicable' $result.Major | Should -Be 3 $result.Minor | Should -Be 2 $result.Patch | Should -Be 1 $result.OriginalPattern | Should -Be '3.2.1' } It 'parses floating patch x.y.* as FloatingPatch' { $result = ConvertTo-VersionPattern -VersionPattern '2.1.*' $result.Type | Should -Be 'FloatingPatch' $result.Major | Should -Be 2 $result.Minor | Should -Be 1 $result.Patch | Should -BeNullOrEmpty } It 'parses floating minor x.* as FloatingMinor' { $result = ConvertTo-VersionPattern -VersionPattern '5.*' $result.Type | Should -Be 'FloatingMinor' $result.Major | Should -Be 5 $result.Minor | Should -BeNullOrEmpty $result.Patch | Should -BeNullOrEmpty } It 'parses version 0.x.y correctly' { $result = ConvertTo-VersionPattern -VersionPattern '0.2.3' $result.Type | Should -Be 'LowestApplicable' $result.Major | Should -Be 0 $result.Minor | Should -Be 2 $result.Patch | Should -Be 3 } It 'throws on invalid pattern' { { ConvertTo-VersionPattern -VersionPattern 'abc' } | Should -Throw '*Invalid version pattern*' } It 'throws on partial pattern x.y' { { ConvertTo-VersionPattern -VersionPattern '1.2' } | Should -Throw '*Invalid version pattern*' } It 'throws on empty string' { { ConvertTo-VersionPattern -VersionPattern '' } | Should -Throw '*Invalid version pattern*' } } Describe 'Test-SemVerCompatibility' { Context 'LowestApplicable pattern' { BeforeAll { $pattern = @{ Type = 'LowestApplicable'; Major = 2; Minor = 1; Patch = 0; OriginalPattern = '2.1.0' } } It 'accepts exact version match' { Test-SemVerCompatibility -Available ([Version]::new(2, 1, 0)) -VersionPattern $pattern | Should -BeTrue } It 'accepts same major, higher minor' { Test-SemVerCompatibility -Available ([Version]::new(2, 3, 0)) -VersionPattern $pattern | Should -BeTrue } It 'accepts same major.minor, higher patch' { Test-SemVerCompatibility -Available ([Version]::new(2, 1, 5)) -VersionPattern $pattern | Should -BeTrue } It 'rejects different major version' { Test-SemVerCompatibility -Available ([Version]::new(3, 0, 0)) -VersionPattern $pattern | Should -BeFalse } It 'rejects lower minor.patch' { Test-SemVerCompatibility -Available ([Version]::new(2, 0, 9)) -VersionPattern $pattern | Should -BeFalse } } Context 'LowestApplicable 0.x special case' { BeforeAll { $pattern = @{ Type = 'LowestApplicable'; Major = 0; Minor = 2; Patch = 1; OriginalPattern = '0.2.1' } } It 'accepts same 0.minor with higher patch' { Test-SemVerCompatibility -Available ([Version]::new(0, 2, 3)) -VersionPattern $pattern | Should -BeTrue } It 'rejects different minor under 0.x' { Test-SemVerCompatibility -Available ([Version]::new(0, 3, 0)) -VersionPattern $pattern | Should -BeFalse } It 'rejects lower patch under 0.x' { Test-SemVerCompatibility -Available ([Version]::new(0, 2, 0)) -VersionPattern $pattern | Should -BeFalse } } Context 'FloatingPatch pattern' { BeforeAll { $pattern = @{ Type = 'FloatingPatch'; Major = 2; Minor = 1; OriginalPattern = '2.1.*' } } It 'accepts any patch within same major.minor' { Test-SemVerCompatibility -Available ([Version]::new(2, 1, 0)) -VersionPattern $pattern | Should -BeTrue Test-SemVerCompatibility -Available ([Version]::new(2, 1, 99)) -VersionPattern $pattern | Should -BeTrue } It 'rejects different minor' { Test-SemVerCompatibility -Available ([Version]::new(2, 2, 0)) -VersionPattern $pattern | Should -BeFalse } It 'rejects different major' { Test-SemVerCompatibility -Available ([Version]::new(3, 1, 0)) -VersionPattern $pattern | Should -BeFalse } } Context 'FloatingPatch 0.x special case' { BeforeAll { $pattern = @{ Type = 'FloatingPatch'; Major = 0; Minor = 3; OriginalPattern = '0.3.*' } } It 'accepts any patch within 0.3' { Test-SemVerCompatibility -Available ([Version]::new(0, 3, 0)) -VersionPattern $pattern | Should -BeTrue Test-SemVerCompatibility -Available ([Version]::new(0, 3, 15)) -VersionPattern $pattern | Should -BeTrue } It 'rejects different minor under 0.x' { Test-SemVerCompatibility -Available ([Version]::new(0, 4, 0)) -VersionPattern $pattern | Should -BeFalse } } Context 'FloatingMinor pattern' { BeforeAll { $pattern = @{ Type = 'FloatingMinor'; Major = 3; OriginalPattern = '3.*' } } It 'accepts any version within same major' { Test-SemVerCompatibility -Available ([Version]::new(3, 0, 0)) -VersionPattern $pattern | Should -BeTrue Test-SemVerCompatibility -Available ([Version]::new(3, 99, 99)) -VersionPattern $pattern | Should -BeTrue } It 'rejects different major' { Test-SemVerCompatibility -Available ([Version]::new(4, 0, 0)) -VersionPattern $pattern | Should -BeFalse Test-SemVerCompatibility -Available ([Version]::new(2, 0, 0)) -VersionPattern $pattern | Should -BeFalse } } Context 'FloatingMinor 0.x special case' { BeforeAll { $pattern = @{ Type = 'FloatingMinor'; Major = 0; OriginalPattern = '0.*' } } It 'accepts any 0.x version' { Test-SemVerCompatibility -Available ([Version]::new(0, 0, 0)) -VersionPattern $pattern | Should -BeTrue Test-SemVerCompatibility -Available ([Version]::new(0, 9, 9)) -VersionPattern $pattern | Should -BeTrue } It 'rejects major > 0' { Test-SemVerCompatibility -Available ([Version]::new(1, 0, 0)) -VersionPattern $pattern | Should -BeFalse } } } Describe 'Get-CompatibleVersionsForPattern' { BeforeAll { $versions = @{ 'v1.0.0' = [Version]::new(1, 0, 0) 'v1.1.0' = [Version]::new(1, 1, 0) 'v1.1.5' = [Version]::new(1, 1, 5) 'v1.2.0' = [Version]::new(1, 2, 0) 'v2.0.0' = [Version]::new(2, 0, 0) } } It 'returns versions >= requested for LowestApplicable' { $pattern = @{ Type = 'LowestApplicable'; Major = 1; Minor = 1; Patch = 0; OriginalPattern = '1.1.0' } $result = Get-CompatibleVersionsForPattern -ParsedVersions $versions -VersionPattern $pattern $result.Count | Should -Be 3 # v1.1.0, v1.1.5, v1.2.0 $result.Tag | Should -Contain 'v1.1.0' $result.Tag | Should -Contain 'v1.1.5' $result.Tag | Should -Contain 'v1.2.0' } It 'returns only matching major.minor for FloatingPatch' { $pattern = @{ Type = 'FloatingPatch'; Major = 1; Minor = 1; OriginalPattern = '1.1.*' } $result = Get-CompatibleVersionsForPattern -ParsedVersions $versions -VersionPattern $pattern $result.Count | Should -Be 2 # v1.1.0, v1.1.5 $result.Tag | Should -Contain 'v1.1.0' $result.Tag | Should -Contain 'v1.1.5' } It 'returns all matching major for FloatingMinor' { $pattern = @{ Type = 'FloatingMinor'; Major = 1; OriginalPattern = '1.*' } $result = Get-CompatibleVersionsForPattern -ParsedVersions $versions -VersionPattern $pattern $result.Count | Should -Be 4 # all v1.x.x $result.Tag | Should -Not -Contain 'v2.0.0' } It 'throws when no compatible version found' { $pattern = @{ Type = 'LowestApplicable'; Major = 3; Minor = 0; Patch = 0; OriginalPattern = '3.0.0' } { Get-CompatibleVersionsForPattern -ParsedVersions $versions -VersionPattern $pattern } | Should -Throw '*No compatible version found*' } } Describe 'Select-VersionFromIntersection' { BeforeAll { $intersectionVersions = @( [PSCustomObject]@{ Tag = 'v1.1.0'; Version = [Version]::new(1, 1, 0) } [PSCustomObject]@{ Tag = 'v1.1.5'; Version = [Version]::new(1, 1, 5) } [PSCustomObject]@{ Tag = 'v1.2.0'; Version = [Version]::new(1, 2, 0) } ) } It 'selects lowest version when all patterns are LowestApplicable' { $patterns = @{ 'caller1' = @{ Type = 'LowestApplicable'; Major = 1; Minor = 1; Patch = 0; OriginalPattern = '1.1.0' } 'caller2' = @{ Type = 'LowestApplicable'; Major = 1; Minor = 0; Patch = 0; OriginalPattern = '1.0.0' } } $result = Select-VersionFromIntersection -IntersectionVersions $intersectionVersions -RequestedPatterns $patterns $result.Tag | Should -Be 'v1.1.0' } It 'selects highest version when any pattern is floating' { $patterns = @{ 'caller1' = @{ Type = 'LowestApplicable'; Major = 1; Minor = 1; Patch = 0; OriginalPattern = '1.1.0' } 'caller2' = @{ Type = 'FloatingPatch'; Major = 1; Minor = 1; OriginalPattern = '1.1.*' } } $result = Select-VersionFromIntersection -IntersectionVersions $intersectionVersions -RequestedPatterns $patterns $result.Tag | Should -Be 'v1.2.0' } } Describe 'Get-SemVersionIntersection' { It 'returns common tags between two sets' { $set1 = @( [PSCustomObject]@{ Tag = 'v1.0.0'; Version = [Version]::new(1, 0, 0) } [PSCustomObject]@{ Tag = 'v1.1.0'; Version = [Version]::new(1, 1, 0) } [PSCustomObject]@{ Tag = 'v1.2.0'; Version = [Version]::new(1, 2, 0) } ) $set2 = @( [PSCustomObject]@{ Tag = 'v1.1.0'; Version = [Version]::new(1, 1, 0) } [PSCustomObject]@{ Tag = 'v1.2.0'; Version = [Version]::new(1, 2, 0) } [PSCustomObject]@{ Tag = 'v2.0.0'; Version = [Version]::new(2, 0, 0) } ) $result = Get-SemVersionIntersection -Set1 $set1 -Set2 $set2 $result.Count | Should -Be 2 $result.Tag | Should -Contain 'v1.1.0' $result.Tag | Should -Contain 'v1.2.0' } It 'returns empty array when no intersection' { $set1 = @( [PSCustomObject]@{ Tag = 'v1.0.0'; Version = [Version]::new(1, 0, 0) } ) $set2 = @( [PSCustomObject]@{ Tag = 'v2.0.0'; Version = [Version]::new(2, 0, 0) } ) $result = Get-SemVersionIntersection -Set1 $set1 -Set2 $set2 $result.Count | Should -Be 0 } } Describe 'Format-SemVersion' { It 'formats Version object as major.minor.patch' { $v = [Version]::new(3, 2, 1) Format-SemVersion -Version $v | Should -Be '3.2.1' } It 'formats zero version correctly' { $v = [Version]::new(0, 0, 0) Format-SemVersion -Version $v | Should -Be '0.0.0' } } Describe 'Get-TagIntersection' { It 'returns common tags' { $result = Get-TagIntersection -Tags1 @('v1.0', 'v1.1', 'v2.0') -Tags2 @('v1.1', 'v2.0', 'v3.0') $result.Count | Should -Be 2 $result | Should -Contain 'v1.1' $result | Should -Contain 'v2.0' } It 'returns empty when no overlap' { $result = Get-TagIntersection -Tags1 @('v1.0') -Tags2 @('v2.0') $result.Count | Should -Be 0 } It 'handles empty arrays' { $result = Get-TagIntersection -Tags1 @() -Tags2 @('v1.0') $result.Count | Should -Be 0 } } Describe 'Get-HostnameFromUrl' { It 'extracts hostname from HTTPS URL' { Get-HostnameFromUrl -Url 'https://github.com/org/repo.git' | Should -Be 'github.com' } It 'extracts hostname from git@ SSH URL' { Get-HostnameFromUrl -Url 'git@github.com:org/repo.git' | Should -Be 'github.com' } It 'extracts hostname from ssh:// URL' { Get-HostnameFromUrl -Url 'ssh://git@gitlab.com/org/repo.git' | Should -Be 'gitlab.com' } It 'extracts hostname from ssh:// URL with port' { Get-HostnameFromUrl -Url 'ssh://git@gitlab.com:2222/org/repo.git' | Should -Be 'gitlab.com' } It 'extracts hostname from HTTP URL' { Get-HostnameFromUrl -Url 'http://internal-git.company.com/repo.git' | Should -Be 'internal-git.company.com' } It 'returns null for unrecognized format' { Get-HostnameFromUrl -Url 'not-a-url' | Should -BeNullOrEmpty } } Describe 'Test-DependencyConfiguration' { It 'passes when no conflict exists' { $newRepo = [PSCustomObject]@{ 'Repository URL' = 'https://github.com/org/repo.git' 'Dependency Resolution' = 'SemVer' } $existingRepo = @{ DependencyResolution = 'SemVer' } { Test-DependencyConfiguration -NewRepo $newRepo -ExistingRepo $existingRepo } | Should -Not -Throw } It 'throws when dependency resolution mode changes' { $newRepo = [PSCustomObject]@{ 'Repository URL' = 'https://github.com/org/repo.git' 'Dependency Resolution' = 'SemVer' } $existingRepo = @{ DependencyResolution = 'Agnostic' } { Test-DependencyConfiguration -NewRepo $newRepo -ExistingRepo $existingRepo } | Should -Throw '*cannot change*' } It 'throws when version regex changes for SemVer mode' { $newRepo = [PSCustomObject]@{ 'Repository URL' = 'https://github.com/org/repo.git' 'Dependency Resolution' = 'SemVer' 'Version Regex' = '^v(\d+)\.(\d+)\.(\d+)-beta$' } $existingRepo = @{ DependencyResolution = 'SemVer' VersionRegex = '^v?(\d+)\.(\d+)\.(\d+)$' } { Test-DependencyConfiguration -NewRepo $newRepo -ExistingRepo $existingRepo } | Should -Throw '*cannot change*' } It 'defaults to Agnostic when Dependency Resolution not specified' { $newRepo = [PSCustomObject]@{ 'Repository URL' = 'https://github.com/org/repo.git' } $existingRepo = @{ DependencyResolution = 'Agnostic' } { Test-DependencyConfiguration -NewRepo $newRepo -ExistingRepo $existingRepo } | Should -Not -Throw } } Describe 'Get-AbsoluteBasePath' { It 'returns rooted path unchanged' { if ($IsWindows) { $result = Get-AbsoluteBasePath -BasePath 'C:\Projects\repo' -DependencyFilePath 'C:\config\deps.json' $result | Should -Be 'C:\Projects\repo' } else { $result = Get-AbsoluteBasePath -BasePath '/tmp/repo' -DependencyFilePath '/config/deps.json' $result | Should -Be '/tmp/repo' } } } Describe 'Export-CheckoutResults' { BeforeEach { # Set up module state for testing & (Get-Module RepoHerd) { $script:SuccessCount = 2 $script:FailureCount = 0 $script:PostCheckoutScriptExecutions = 0 $script:PostCheckoutScriptFailures = 0 $script:PostCheckoutScriptsEnabled = $false $script:RecursiveMode = $true $script:MaxDepth = 5 $script:DefaultApiCompatibility = 'Permissive' $script:DefaultDependencyFileName = 'dependencies.json' $script:DryRun = $false $script:ProcessedDependencyFiles = @('C:\test\dependencies.json') $script:ErrorMessages = @() $script:RepositoryDictionary = @{ 'https://github.com/org/repoA.git' = @{ AbsolutePath = 'C:\test\repo-a' DependencyResolution = 'Agnostic' Tag = 'v1.0.0' AlreadyCheckedOut = $true NeedCheckout = $false CheckoutFailed = $false RequestedBy = @('root-dependency-file') } 'https://github.com/org/repoB.git' = @{ AbsolutePath = 'C:\test\repo-b' DependencyResolution = 'SemVer' AlreadyCheckedOut = $true NeedCheckout = $false CheckoutFailed = $false SelectedTag = 'v3.0.0' SelectedVersion = [Version]::new(3, 0, 0) RequestedPatterns = @{ 'root-dependency-file' = @{ OriginalPattern = '3.0.0'; Type = 'LowestApplicable' } } RequestedBy = @('root-dependency-file') } } } } It 'writes valid JSON with correct schema version' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $outputFile | Should -Exist $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.schemaVersion | Should -Be '1.0.0' } It 'includes correct metadata' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.metadata.toolVersion | Should -Be '9.1.0' $result.metadata.recursiveMode | Should -Be $true $result.metadata.maxDepth | Should -Be 5 $result.metadata.apiCompatibility | Should -Be 'Permissive' $result.metadata.inputFile | Should -Be 'dependencies.json' } It 'includes correct summary counters' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.summary.success | Should -Be $true $result.summary.successCount | Should -Be 2 $result.summary.failureCount | Should -Be 0 $result.summary.totalRepositories | Should -Be 2 } It 'includes repository entries with correct fields' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.repositories.Count | Should -Be 2 foreach ($repo in $result.repositories) { $repo.url | Should -Not -BeNullOrEmpty $repo.path | Should -Not -BeNullOrEmpty $repo.dependencyResolution | Should -BeIn @('SemVer', 'Agnostic') $repo.status | Should -BeIn @('success', 'failed', 'skipped') } } It 'populates SemVer-specific fields for SemVer repos' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $semVerRepo = $result.repositories | Where-Object { $_.dependencyResolution -eq 'SemVer' } $semVerRepo | Should -Not -BeNullOrEmpty $semVerRepo.tag | Should -Be 'v3.0.0' $semVerRepo.selectedVersion | Should -Be '3.0.0' $semVerRepo.requestedVersion | Should -Be '3.0.0' } It 'sets null for SemVer fields on Agnostic repos' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $agnosticRepo = $result.repositories | Where-Object { $_.dependencyResolution -eq 'Agnostic' } $agnosticRepo | Should -Not -BeNullOrEmpty $agnosticRepo.tag | Should -Be 'v1.0.0' $agnosticRepo.requestedVersion | Should -BeNullOrEmpty $agnosticRepo.selectedVersion | Should -BeNullOrEmpty } It 'includes error messages when failures occur' { & (Get-Module RepoHerd) { $script:FailureCount = 1 $script:ErrorMessages = @('Repository clone failed', 'Tag not found') } $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.summary.success | Should -Be $false $result.errors.Count | Should -Be 2 $result.errors[0] | Should -Be 'Repository clone failed' } It 'handles empty repository dictionary' { & (Get-Module RepoHerd) { $script:RepositoryDictionary = @{} $script:SuccessCount = 0 $script:ProcessedDependencyFiles = @() } $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.repositories.Count | Should -Be 0 $result.summary.totalRepositories | Should -Be 0 } It 'includes requestedBy field for each repository' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json foreach ($repo in $result.repositories) { $repo.requestedBy | Should -Not -BeNullOrEmpty -Because "every repo should have a requestedBy" $repo.requestedBy | Should -Contain 'root-dependency-file' } } It 'includes postCheckoutScript field when script was tracked' { & (Get-Module RepoHerd) { $script:RepositoryDictionary['https://github.com/org/repoA.git'].PostCheckoutScript = @{ Configured = $true ScriptPath = 'C:\test\repo-a\post-checkout.ps1' Found = $true Executed = $false Status = 'skipped' Reason = 'Disabled globally via -DisablePostCheckoutScripts' } } $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $repoWithScript = $result.repositories | Where-Object { $_.url -eq 'https://github.com/org/repoA.git' } $repoWithScript.postCheckoutScript | Should -Not -BeNullOrEmpty $repoWithScript.postCheckoutScript.configured | Should -Be $true $repoWithScript.postCheckoutScript.found | Should -Be $true $repoWithScript.postCheckoutScript.executed | Should -Be $false $repoWithScript.postCheckoutScript.status | Should -Be 'skipped' $repoWithScript.postCheckoutScript.reason | Should -Be 'Disabled globally via -DisablePostCheckoutScripts' } It 'sets postCheckoutScript to null when no script configured' { $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $repoWithout = $result.repositories | Where-Object { $_.url -eq 'https://github.com/org/repoB.git' } $repoWithout.postCheckoutScript | Should -BeNullOrEmpty } It 'includes rootPostCheckoutScripts for depth-0 scripts' { & (Get-Module RepoHerd) { $script:PostCheckoutScriptResults = @( @{ Configured = $true ScriptPath = 'C:\test\build\config\post-checkout.ps1' Found = $false Executed = $false Status = 'skipped' Reason = 'Disabled globally via -DisablePostCheckoutScripts' RepositoryUrl = '' } ) } $outputFile = Join-Path $TestDrive 'result.json' Export-CheckoutResults -OutputFile $outputFile $result = Get-Content $outputFile -Raw | ConvertFrom-Json $result.rootPostCheckoutScripts.Count | Should -Be 1 $result.rootPostCheckoutScripts[0].configured | Should -Be $true $result.rootPostCheckoutScripts[0].status | Should -Be 'skipped' } } Describe 'Test-SshTransportAvailable' { It 'returns true when ssh is available on this system' { # On macOS/Linux, ssh should always be present if (-not $IsWindows) { Test-SshTransportAvailable | Should -Be $true } else { Set-ItResult -Skipped -Because 'this test validates OpenSSH availability on Unix' } } } Describe 'Set-GitSshKey' { BeforeAll { # Create a temporary OpenSSH key for testing (no passphrase) $script:testKeyPath = Join-Path $TestDrive 'test_key' ssh-keygen -t ed25519 -f $script:testKeyPath -N '""' -q 2>$null if (-not (Test-Path $script:testKeyPath)) { # Fallback: create a fake OpenSSH-format key file Set-Content -Path $script:testKeyPath -Value "-----BEGIN OPENSSH PRIVATE KEY-----`ntest`n-----END OPENSSH PRIVATE KEY-----" } # Create a fake PuTTY key for rejection testing $script:testPuttyKeyPath = Join-Path $TestDrive 'test_key.ppk' Set-Content -Path $script:testPuttyKeyPath -Value "PuTTY-User-Key-File-3: ssh-ed25519`nfake-putty-key-content" } Context 'OpenSSH path (macOS/Linux)' { BeforeAll { if ($IsWindows) { $script:skipUnix = $true } } It 'configures GIT_SSH_COMMAND with explicit key path' { if ($script:skipUnix) { Set-ItResult -Skipped -Because 'OpenSSH tests only run on macOS/Linux' return } # Save and clear env vars $originalSshCmd = $env:GIT_SSH_COMMAND $originalSsh = $env:GIT_SSH $env:GIT_SSH_COMMAND = $null $env:GIT_SSH = $null try { $result = Set-GitSshKey -SshKeyPath $script:testKeyPath $result | Should -Be $true $env:GIT_SSH_COMMAND | Should -BeLike "ssh -i *test_key* -o IdentitiesOnly=yes" $env:GIT_SSH | Should -BeNullOrEmpty } finally { $env:GIT_SSH_COMMAND = $originalSshCmd $env:GIT_SSH = $originalSsh } } It 'rejects PuTTY format keys' { if ($script:skipUnix) { Set-ItResult -Skipped -Because 'OpenSSH tests only run on macOS/Linux' return } $result = Set-GitSshKey -SshKeyPath $script:testPuttyKeyPath $result | Should -Be $false } It 'returns false for non-existent key file' { $result = Set-GitSshKey -SshKeyPath (Join-Path $TestDrive 'nonexistent_key') $result | Should -Be $false } } Context 'Permission check (macOS/Linux)' { It 'warns on overly permissive key file' { if ($IsWindows) { Set-ItResult -Skipped -Because 'Unix permission tests only run on macOS/Linux' return } # Make key world-readable chmod 644 $script:testKeyPath # Save env $originalSshCmd = $env:GIT_SSH_COMMAND $env:GIT_SSH_COMMAND = $null try { # Should still succeed but produce a warning $result = Set-GitSshKey -SshKeyPath $script:testKeyPath $result | Should -Be $true } finally { chmod 600 $script:testKeyPath $env:GIT_SSH_COMMAND = $originalSshCmd } } } } |