functions/_copyFolderFromGitRepo.Tests.ps1
|
# <copyright file="_copyFolderFromGitRepo.Tests.ps1" company="Endjin Limited"> # Copyright (c) Endjin Limited. All rights reserved. # </copyright> BeforeAll { # sut . $PSCommandPath.Replace('.Tests.ps1','.ps1') Set-StrictMode -Version Latest # Runs a git command to build the fixture repositories, failing the test setup if it does not succeed function invokeFixtureGit { $output = & git @args 2>&1 if ($LASTEXITCODE -ne 0) { throw "git $($args -join ' ') failed: $($output -join "`n")" } $output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] } } # Writes a file with exact content (i.e. no platform-specific line endings are added) function setFixtureFile([string] $Path, [string] $Content) { New-Item -ItemType Directory -Path (Split-Path -Parent $Path) -Force | Out-Null [IO.File]::WriteAllText($Path, $Content) } # Sets git configuration via environment variables, which mimics settings in the user's git configuration function setGitEnvironmentConfig([hashtable] $Settings) { $i = 0 foreach ($key in $Settings.Keys) { Set-Item -Path "env:GIT_CONFIG_KEY_$i" -Value $key Set-Item -Path "env:GIT_CONFIG_VALUE_$i" -Value $Settings[$key] $i++ } $env:GIT_CONFIG_COUNT = $i } function clearGitEnvironmentConfig { Get-ChildItem env:GIT_CONFIG_* | ForEach-Object { Remove-Item -Path "env:$($_.Name)" } } } Describe '_copyFolderFromGitRepo' { BeforeAll { # Build a source repository with a module folder, a tag, a branch containing a '/' and several commits: # c1 -> c2 (tag v2.0.0) -> c3 (main) # \-> c4 (feature/test) $sourceRepo = Join-Path $TestDrive 'source' invokeFixtureGit -c init.defaultBranch=main init --quiet $sourceRepo | Out-Null invokeFixtureGit -C $sourceRepo config user.email 'test@example.com' | Out-Null invokeFixtureGit -C $sourceRepo config user.name 'Test' | Out-Null invokeFixtureGit -C $sourceRepo config core.autocrlf false | Out-Null setFixtureFile (Join-Path $sourceRepo '.gitattributes') "* text=auto`n" setFixtureFile (Join-Path $sourceRepo 'README.md') "Not part of the extension`n" setFixtureFile (Join-Path $sourceRepo 'module' 'Fixture.psd1') "@{ ModuleVersion = '1.0.0' }`n" setFixtureFile (Join-Path $sourceRepo 'module' 'tasks' 'sample.tasks.ps1') "line1`nline2`n" setFixtureFile (Join-Path $sourceRepo 'module' 'version.txt') "c1`n" invokeFixtureGit -C $sourceRepo add -A | Out-Null invokeFixtureGit -C $sourceRepo commit --quiet -m 'c1' | Out-Null $commit1 = invokeFixtureGit -C $sourceRepo rev-parse HEAD setFixtureFile (Join-Path $sourceRepo 'module' 'version.txt') "c2`n" invokeFixtureGit -C $sourceRepo commit --quiet -am 'c2' | Out-Null invokeFixtureGit -C $sourceRepo tag -a 'v2.0.0' -m 'v2.0.0' | Out-Null $commit2 = invokeFixtureGit -C $sourceRepo rev-parse HEAD setFixtureFile (Join-Path $sourceRepo 'module' 'version.txt') "c3`n" invokeFixtureGit -C $sourceRepo commit --quiet -am 'c3' | Out-Null $commit3 = invokeFixtureGit -C $sourceRepo rev-parse HEAD invokeFixtureGit -C $sourceRepo checkout --quiet -b 'feature/test' $commit2 | Out-Null setFixtureFile (Join-Path $sourceRepo 'module' 'version.txt') "c4`n" invokeFixtureGit -C $sourceRepo commit --quiet -am 'c4' | Out-Null $commit4 = invokeFixtureGit -C $sourceRepo rev-parse HEAD invokeFixtureGit -C $sourceRepo checkout --quiet main | Out-Null # Serve it as a bare repository via a 'file://' URL, so that git uses its network protocol $bareRepo = Join-Path $TestDrive 'fixture.git' invokeFixtureGit clone --bare --quiet $sourceRepo $bareRepo | Out-Null $repoUrl = [uri]::new($bareRepo, [UriKind]::Absolute).AbsoluteUri $targetPath = Join-Path $TestDrive 'extensions' Mock Write-Verbose {} } Context 'Fetching a git reference' { It 'Copies the folder from <GitRef> and returns the commit it resolved to' -ForEach @( @{ GitRef = 'main'; ExpectedCommit = 'commit3'; ExpectedContent = 'c3' } @{ GitRef = 'refs/heads/main'; ExpectedCommit = 'commit3'; ExpectedContent = 'c3' } @{ GitRef = 'feature/test'; ExpectedCommit = 'commit4'; ExpectedContent = 'c4' } @{ GitRef = 'v2.0.0'; ExpectedCommit = 'commit2'; ExpectedContent = 'c2' } @{ GitRef = 'refs/tags/v2.0.0'; ExpectedCommit = 'commit2'; ExpectedContent = 'c2' } ) { $destination = Join-Path $targetPath "ref-$($GitRef.Replace('/', '-'))" $result = _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination -GitRef $GitRef $result | Should -Be (Get-Variable -Name $ExpectedCommit -ValueOnly) Get-Content -Raw (Join-Path $destination 'version.txt') | Should -Be "$ExpectedContent`n" } It 'Copies a commit specified as the git reference' { $destination = Join-Path $targetPath 'ref-commit' $result = _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination -GitRef $commit1 $result | Should -Be $commit1 Get-Content -Raw (Join-Path $destination 'version.txt') | Should -Be "c1`n" } It 'Copies only the contents of the requested folder' { $destination = Join-Path $targetPath 'folder-only' _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination | Out-Null Join-Path $destination 'Fixture.psd1' | Should -Exist Join-Path $destination 'tasks' 'sample.tasks.ps1' | Should -Exist Join-Path $destination 'module' | Should -Not -Exist Join-Path $destination 'README.md' | Should -Not -Exist } It 'Does not copy the git metadata when the whole repository is copied' { $destination = Join-Path $targetPath 'whole-repo' _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath '.' -DestinationPath $destination | Out-Null Join-Path $destination 'module' 'Fixture.psd1' | Should -Exist Join-Path $destination '.git' | Should -Not -Exist } } Context 'Fetching a specific commit' { It 'Copies the folder from the specified commit, rather than the latest commit of the git reference' { $destination = Join-Path $targetPath 'pinned' $result = _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination -GitRef 'main' -Commit $commit1 $result | Should -Be $commit1 Get-Content -Raw (Join-Path $destination 'version.txt') | Should -Be "c1`n" } Context 'When the git server does not allow a specific commit to be fetched' { BeforeAll { # The original git protocol only allows advertised objects (e.g. branch tips) to be fetched setGitEnvironmentConfig @{ 'protocol.version' = '0' } } AfterAll { clearGitEnvironmentConfig } It 'Falls back to fetching the history of the git reference' { $destination = Join-Path $targetPath 'pinned-fallback' $result = _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination -GitRef 'main' -Commit $commit1 $result | Should -Be $commit1 Get-Content -Raw (Join-Path $destination 'version.txt') | Should -Be "c1`n" Should -Invoke Write-Verbose -ParameterFilter { $Message -like 'Unable to fetch commit*' } -Times 1 } } } Context 'Replacing an existing copy' { It 'Replaces the existing folder rather than copying into it' { $destination = Join-Path $targetPath 'existing' setFixtureFile (Join-Path $destination 'stale.txt') "stale`n" _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination | Out-Null Join-Path $destination 'Fixture.psd1' | Should -Exist Join-Path $destination 'stale.txt' | Should -Not -Exist Join-Path $destination 'module' | Should -Not -Exist } } Context 'Line endings' { BeforeAll { # Mimic a Windows user's typical git configuration, which converts line endings on checkout setGitEnvironmentConfig @{ 'core.autocrlf' = 'true' } } AfterAll { clearGitEnvironmentConfig } It 'Ignores the user''s line-ending settings so that the copied files are the same on every machine' { $destination = Join-Path $targetPath 'line-endings' _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath $destination | Out-Null $bytes = [IO.File]::ReadAllBytes((Join-Path $destination 'tasks' 'sample.tasks.ps1')) $bytes | Should -Not -Contain ([byte][char]"`r") } } Context 'Error handling' { BeforeAll { Mock New-Guid { 'zf-copy-folder-test' } $expectedTempDir = Join-Path ([System.IO.Path]::GetTempPath()) 'zf-copy-folder-test' } It 'Throws when the git CLI is not available' { { _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath (Join-Path $targetPath 'no-git') -GitCmd 'not-a-real-git-command' } | Should -Throw '*Git CLI is not installed*' } It 'Throws with the git output when the git reference does not exist' { { _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'module' -DestinationPath (Join-Path $targetPath 'bad-ref') -GitRef 'does-not-exist' } | Should -Throw '*git fetch*does-not-exist*' } It 'Throws when the folder does not exist in the repository' { { _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'not-a-folder' -DestinationPath (Join-Path $targetPath 'bad-folder') } | Should -Throw "*The folder 'not-a-folder' does not exist*" } It 'Removes the temporary folder when an error occurs' { { _copyFolderFromGitRepo -RepoUrl $repoUrl -RepoFolderPath 'not-a-folder' -DestinationPath (Join-Path $targetPath 'cleanup') } | Should -Throw $expectedTempDir | Should -Not -Exist } } } |