functions/Get-EndjinGist.Tests.ps1
|
# <copyright file="Get-EndjinGist.Tests.ps1" company="Endjin Limited"> # Copyright (c) Endjin Limited. All rights reserved. # </copyright> Describe "Get-EndjinGist" { BeforeAll { # Define stubs for external module functions to avoid dependency and ensure mocking works function ConvertTo-Yaml { param($Data, $OutFile, [switch]$Force) } function ConvertFrom-Yaml { param([switch]$Ordered) } function _Get-VendirPath { } function _Install-Vendir { } $sut = "$PSScriptRoot\Get-EndjinGist.ps1" . $sut # Mock Data $mockGistMap = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('path/to/include') } ) } # Create a real temp file so ValidateScript({Test-Path $_ -PathType Leaf}) passes $dummyGistMapPath = Join-Path $TestDrive 'gist-map.yml' Set-Content -Path $dummyGistMapPath -Value 'placeholder' } Context "Prerequisites" { It "Should call _Get-VendirPath to resolve vendir" { Mock _Get-VendirPath { return 'vendir' } Mock Get-Content { return "yaml content" } Mock ConvertFrom-Yaml { return @{ 'test-group' = @(@{ name = 'test-gist'; source = 'https://github.com/org/repo.git'; ref = 'main'; includePaths = @('path/to/include') }) } } Mock ConvertTo-Yaml { } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath Should -Invoke _Get-VendirPath -Times 1 } It "Should propagate exceptions from _Get-VendirPath" { Mock _Get-VendirPath { throw "Failed to download vendir from https://example.com: error" } { Get-EndjinGist -Group 'any' -Name 'any' -GistMapPath $dummyGistMapPath } | Should -Throw "*Failed to download vendir*" } } Context "Input Validation" { BeforeAll { Mock _Get-VendirPath { return 'vendir' } Mock Get-Content { return "yaml content" } Mock ConvertFrom-Yaml { return $mockGistMap } } It "Should throw if Group is unknown" { { Get-EndjinGist -Group 'unknown-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "Unknown gist group: unknown-group" } It "Should throw if Name is unknown in Group" { { Get-EndjinGist -Group 'test-group' -Name 'unknown-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "Unknown gist 'unknown-gist' in group 'test-group'" } It "Should throw when GistMapPath does not exist" { { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath '/nonexistent/path/gist-map.yml' } | Should -Throw "*Cannot validate argument on parameter 'GistMapPath'*" } } Context "Execution" { BeforeAll { Mock _Get-VendirPath { return 'vendir' } Mock Get-Content { return "yaml content" } Mock ConvertFrom-Yaml { return $mockGistMap } Mock ConvertTo-Yaml { } Mock Remove-Item { } Mock Write-Host { } } It "Should invoke vendir with generated config" { Mock Invoke-Command { $global:LASTEXITCODE = 0 return "Success" } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath Assert-MockCalled ConvertTo-Yaml -Times 1 Assert-MockCalled Invoke-Command -Times 1 } It "Should handle vendir failure" { Mock Invoke-Command { # Simulate failure $global:LASTEXITCODE = 1 return "Error message" } { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "Operation failed with exit code 1.`nError message" } It "Should throw access denied error with VSCode guidance when vendir output contains 'Access is denied'" { Mock Invoke-Command { $global:LASTEXITCODE = 1 return "Syncing directory '.endjin/test-group/test-gist'... Access is denied" } { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Throw "vendir sync failed with an access denied error*" } } Context "Vendir Config Generation" { BeforeAll { Mock _Get-VendirPath { return 'vendir' } Mock Get-Content { return "yaml content" } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Mock Write-Host { } } It "Should set newRootPath in vendir config when gist has sourceDirectory" { $mockGistMapWithSourceDir = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('path/to/include') sourceDirectory = 'src/subdir' } ) } Mock ConvertFrom-Yaml { return $mockGistMapWithSourceDir } $capturedData = $null Mock ConvertTo-Yaml { $script:capturedData = $Data } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath $script:capturedData.directories[0].contents[0].newRootPath | Should -Be 'src/subdir' } It "Should not set newRootPath when gist lacks sourceDirectory" { Mock ConvertFrom-Yaml { return $mockGistMap } $capturedData = $null Mock ConvertTo-Yaml { $script:capturedData = $Data } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath $script:capturedData.directories[0].contents[0].Keys | Should -Not -Contain 'newRootPath' } } Context "MovePaths Processing" { BeforeAll { $mockGistMapWithMovePaths = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('content/**/*') movePaths = @( @{ from = 'content/**/*' to = '${repoRoot}/target/' } ) } ) } Mock _Get-VendirPath { return 'vendir' } Mock Get-Content { return "yaml content" } Mock ConvertTo-Yaml { } Mock Write-Host { } } It "Should expand repoRoot variable in destination path" { Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } # Mock git to return a specific repo root Mock git { return '/mock/repo/root' } -ParameterFilter { $args[0] -eq 'rev-parse' } # Mock Get-ChildItem to return no files (avoiding actual file operations) Mock Get-ChildItem { return @() } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath # Verify the function executed without errors (variable expansion happened internally) Should -Invoke Invoke-Command -Times 1 } It "Should not process moves if movePaths is not defined" { $mockGistMapWithoutMovePaths = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('content/**/*') } ) } Mock ConvertFrom-Yaml { return $mockGistMapWithoutMovePaths } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Mock Move-Item { } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath # Move-Item should never be called when movePaths is not defined Should -Invoke Move-Item -Times 0 } It "Should move files and preserve directory structure" { Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' } # Setup source files in TestDrive $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist/content' $subDir = Join-Path $sourceDir 'subfolder' New-Item -ItemType Directory -Path $subDir -Force | Out-Null Set-Content -Path (Join-Path $sourceDir 'file1.txt') -Value 'content1' Set-Content -Path (Join-Path $subDir 'file2.txt') -Value 'content2' # Run in TestDrive context Push-Location $TestDrive try { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } finally { Pop-Location } # Verify files were moved to target with preserved structure $targetFile1 = Join-Path $TestDrive 'target/file1.txt' $targetFile2 = Join-Path $TestDrive 'target/subfolder/file2.txt' Test-Path $targetFile1 | Should -Be $true Test-Path $targetFile2 | Should -Be $true # Use [System.IO.File] to bypass the Get-Content mock [System.IO.File]::ReadAllText($targetFile1).Trim() | Should -Be 'content1' [System.IO.File]::ReadAllText($targetFile2).Trim() | Should -Be 'content2' } It "Should clean up empty source directories after move" { Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } -ParameterFilter { $Path -like '*vendir*' -or $Path -like '*.yml' } Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' } # Setup source files in TestDrive $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist/content' New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null Set-Content -Path (Join-Path $sourceDir 'file.txt') -Value 'content' Push-Location $TestDrive try { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } finally { Pop-Location } # Source directory should be cleaned up (empty after move) Test-Path $sourceDir | Should -Be $false } It "Should move a single file when movePaths source is a file" { $mockGistMapWithFileMovePath = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('somefile.txt') movePaths = @( @{ from = 'somefile.txt' to = '${repoRoot}/target/renamed.txt' } ) } ) } Mock ConvertFrom-Yaml { return $mockGistMapWithFileMovePath } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' } # Setup a single source file in TestDrive $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist' New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null Set-Content -Path (Join-Path $sourceDir 'somefile.txt') -Value 'file-content' Push-Location $TestDrive try { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } finally { Pop-Location } # Verify the file was moved to the explicit destination $targetFile = Join-Path $TestDrive 'target/renamed.txt' Test-Path $targetFile | Should -Be $true [System.IO.File]::ReadAllText($targetFile).Trim() | Should -Be 'file-content' # Verify the source file was removed Test-Path (Join-Path $sourceDir 'somefile.txt') | Should -Be $false } It "Should append filename to destination when destination ends with a path separator" { $mockGistMapWithTrailingSlash = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('somefile.txt') movePaths = @( @{ from = 'somefile.txt' to = '${repoRoot}/target/' } ) } ) } Mock ConvertFrom-Yaml { return $mockGistMapWithTrailingSlash } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' } # Setup a single source file in TestDrive $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist' New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null Set-Content -Path (Join-Path $sourceDir 'somefile.txt') -Value 'trailing-slash-content' Push-Location $TestDrive try { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } finally { Pop-Location } # Verify the filename was appended to the trailing-slash destination $targetFile = Join-Path $TestDrive 'target/somefile.txt' Test-Path $targetFile | Should -Be $true [System.IO.File]::ReadAllText($targetFile).Trim() | Should -Be 'trailing-slash-content' # Verify the source file was removed Test-Path (Join-Path $sourceDir 'somefile.txt') | Should -Be $false } It "Should skip gracefully when movePaths source path does not exist" { $mockGistMapWithMissingSource = @{ 'test-group' = @( @{ name = 'test-gist' source = 'https://github.com/org/repo.git' ref = 'main' includePaths = @('nonexistent/**/*') movePaths = @( @{ from = 'nonexistent/**/*' to = '${repoRoot}/target/' } ) } ) } Mock ConvertFrom-Yaml { return $mockGistMapWithMissingSource } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } Mock Move-Item { } Mock git { return $TestDrive } -ParameterFilter { $args[0] -eq 'rev-parse' } # Ensure the source path does NOT exist in TestDrive Push-Location $TestDrive try { { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } | Should -Not -Throw } finally { Pop-Location } Should -Invoke Move-Item -Times 0 } It "Should fall back to PWD when git rev-parse returns nothing" { Mock ConvertFrom-Yaml { return $mockGistMapWithMovePaths } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Remove-Item { } # Mock git to return $null (simulating no git repo) Mock git { return $null } -ParameterFilter { $args[0] -eq 'rev-parse' } # Setup source file in TestDrive $sourceDir = Join-Path $TestDrive '.endjin/test-group/test-gist/content' New-Item -ItemType Directory -Path $sourceDir -Force | Out-Null Set-Content -Path (Join-Path $sourceDir 'file1.txt') -Value 'fallback-content' Push-Location $TestDrive try { Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath } finally { Pop-Location } # PWD was TestDrive, so ${repoRoot} should have resolved to TestDrive $targetFile = Join-Path $TestDrive 'target/file1.txt' Test-Path $targetFile | Should -Be $true [System.IO.File]::ReadAllText($targetFile).Trim() | Should -Be 'fallback-content' } } Context "Cleanup Behavior" { BeforeAll { Mock _Get-VendirPath { return 'vendir' } Mock Get-Content { return "yaml content" } Mock ConvertFrom-Yaml { return $mockGistMap } Mock ConvertTo-Yaml { } Mock Invoke-Command { $global:LASTEXITCODE = 0; return "Success" } Mock Write-Host { } } It "Should remove temp files when not running with -Verbose" { Mock Remove-Item { } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath Should -Invoke Remove-Item -Times 2 } It "Should preserve temp files when running with -Verbose" { Mock Remove-Item { } Get-EndjinGist -Group 'test-group' -Name 'test-gist' -GistMapPath $dummyGistMapPath -Verbose Should -Invoke Remove-Item -Times 0 } } } |