microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/tests/linting/Markdown-Link-Check.Tests.ps1
350lines · modecode
| 1 | #Requires -Modules Pester |
| 2 | # Copyright (c) Microsoft Corporation. |
| 3 | # SPDX-License-Identifier: MIT |
| 4 | <# |
| 5 | .SYNOPSIS |
| 6 | Pester tests for Markdown-Link-Check.ps1 script |
| 7 | .DESCRIPTION |
| 8 | Tests for markdown link checking wrapper functions: |
| 9 | - Get-MarkdownTarget |
| 10 | - Get-RelativePrefix |
| 11 | #> |
| 12 | |
| 13 | BeforeAll { |
| 14 | $script:ScriptPath = Join-Path $PSScriptRoot '../../linting/Markdown-Link-Check.ps1' |
| 15 | . $script:ScriptPath |
| 16 | |
| 17 | # Import LintingHelpers for mocking |
| 18 | Import-Module (Join-Path $PSScriptRoot '../../linting/Modules/LintingHelpers.psm1') -Force |
| 19 | |
| 20 | $script:FixtureDir = Join-Path $PSScriptRoot '../Fixtures/Linting' |
| 21 | } |
| 22 | |
| 23 | AfterAll { |
| 24 | Remove-Module LintingHelpers -Force -ErrorAction SilentlyContinue |
| 25 | } |
| 26 | |
| 27 | #region Get-MarkdownTarget Tests |
| 28 | |
| 29 | Describe 'Get-MarkdownTarget' -Tag 'Unit' { |
| 30 | BeforeAll { |
| 31 | # Create a temp directory to use as test input |
| 32 | $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) |
| 33 | New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null |
| 34 | } |
| 35 | |
| 36 | AfterAll { |
| 37 | Remove-Item -Path $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue |
| 38 | } |
| 39 | |
| 40 | Context 'Git-tracked files in repository' { |
| 41 | BeforeEach { |
| 42 | # Create test markdown files |
| 43 | $script:TestFile1 = Join-Path $script:TempDir 'test1.md' |
| 44 | $script:TestFile2 = Join-Path $script:TempDir 'test2.md' |
| 45 | Set-Content -Path $script:TestFile1 -Value '# Test 1' |
| 46 | Set-Content -Path $script:TestFile2 -Value '# Test 2' |
| 47 | |
| 48 | # Mock git to indicate we're in a repo and return tracked files |
| 49 | Mock git { |
| 50 | if ($args -contains 'rev-parse') { |
| 51 | $global:LASTEXITCODE = 0 |
| 52 | return $script:TempDir |
| 53 | } |
| 54 | elseif ($args -contains 'ls-files') { |
| 55 | $global:LASTEXITCODE = 0 |
| 56 | return @('test1.md', 'test2.md') |
| 57 | } |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | It 'Returns markdown files when given a directory' { |
| 62 | $result = Get-MarkdownTarget -InputPath $script:TempDir |
| 63 | $result | Should -Not -BeNullOrEmpty |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | Context 'Non-git fallback mode' { |
| 68 | BeforeEach { |
| 69 | # Create test files |
| 70 | $script:TestFile = Join-Path $script:TempDir 'readme.md' |
| 71 | Set-Content -Path $script:TestFile -Value '# Readme' |
| 72 | |
| 73 | # Mock git to simulate not being in a repo |
| 74 | Mock git { |
| 75 | $global:LASTEXITCODE = 128 |
| 76 | return 'fatal: not a git repository' |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | It 'Falls back to filesystem when not in git repo' { |
| 81 | $result = Get-MarkdownTarget -InputPath $script:TempDir |
| 82 | $result | Should -Not -BeNullOrEmpty |
| 83 | } |
| 84 | |
| 85 | It 'Returns absolute paths' { |
| 86 | $result = Get-MarkdownTarget -InputPath $script:TempDir |
| 87 | if ($result) { |
| 88 | [System.IO.Path]::IsPathRooted($result[0]) | Should -BeTrue |
| 89 | } |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | Context 'Empty input handling' { |
| 94 | It 'Returns empty array for null input' { |
| 95 | $result = Get-MarkdownTarget -InputPath $null |
| 96 | $result | Should -BeNullOrEmpty |
| 97 | } |
| 98 | |
| 99 | It 'Returns empty array for empty string input' { |
| 100 | $result = Get-MarkdownTarget -InputPath '' |
| 101 | $result | Should -BeNullOrEmpty |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | Context 'Fixture exclusion filtering' { |
| 106 | BeforeEach { |
| 107 | # Create test files including fixture path |
| 108 | $script:IncludeFile = Join-Path $script:TempDir 'docs' 'readme.md' |
| 109 | $script:ExcludeFile = Join-Path $script:TempDir 'scripts' 'tests' 'Fixtures' 'test.md' |
| 110 | |
| 111 | New-Item -ItemType Directory -Path (Join-Path $script:TempDir 'docs') -Force | Out-Null |
| 112 | New-Item -ItemType Directory -Path (Join-Path $script:TempDir 'scripts' 'tests' 'Fixtures') -Force | Out-Null |
| 113 | Set-Content -Path $script:IncludeFile -Value '# Include This' |
| 114 | Set-Content -Path $script:ExcludeFile -Value '# Exclude Fixture' |
| 115 | |
| 116 | # Mock git to simulate repository with tracked files including fixtures |
| 117 | Mock git { |
| 118 | if ($args -contains 'rev-parse') { |
| 119 | $global:LASTEXITCODE = 0 |
| 120 | return $script:TempDir |
| 121 | } |
| 122 | elseif ($args -contains 'ls-files') { |
| 123 | $global:LASTEXITCODE = 0 |
| 124 | # Return both fixture and non-fixture files |
| 125 | return @('docs/readme.md', 'scripts/tests/Fixtures/test.md') |
| 126 | } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | It 'Filters out test fixture files from results' { |
| 131 | # Act |
| 132 | $result = Get-MarkdownTarget -InputPath $script:TempDir |
| 133 | |
| 134 | # Assert - Should exclude files in scripts/tests/Fixtures/ |
| 135 | $fixtureFiles = $result | Where-Object { $_ -like '*Fixtures*' } |
| 136 | $fixtureFiles | Should -BeNullOrEmpty |
| 137 | } |
| 138 | |
| 139 | It 'Includes non-fixture files in results' { |
| 140 | # Act |
| 141 | $result = Get-MarkdownTarget -InputPath $script:TempDir |
| 142 | |
| 143 | # Assert - Should include docs files |
| 144 | $docsFiles = $result | Where-Object { $_ -like '*docs*readme.md' } |
| 145 | $docsFiles | Should -Not -BeNullOrEmpty |
| 146 | } |
| 147 | |
| 148 | It 'Correctly applies the notlike filter pattern' { |
| 149 | # Test the exact filter pattern used in the code |
| 150 | $testPaths = @('docs/readme.md', 'scripts/tests/Fixtures/test.md', 'src/guide.md') |
| 151 | $filtered = $testPaths | Where-Object { $_ -notlike 'scripts/tests/Fixtures/*' } |
| 152 | |
| 153 | $filtered | Should -Contain 'docs/readme.md' |
| 154 | $filtered | Should -Contain 'src/guide.md' |
| 155 | $filtered | Should -Not -Contain 'scripts/tests/Fixtures/test.md' |
| 156 | } |
| 157 | } |
| 158 | } |
| 159 | |
| 160 | #endregion |
| 161 | |
| 162 | #region Get-RelativePrefix Tests |
| 163 | |
| 164 | Describe 'Get-RelativePrefix' -Tag 'Unit' { |
| 165 | BeforeAll { |
| 166 | # Create a temp directory structure for testing relative paths |
| 167 | $script:TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString()) |
| 168 | New-Item -ItemType Directory -Path $script:TempRoot -Force | Out-Null |
| 169 | New-Item -ItemType Directory -Path (Join-Path $script:TempRoot 'docs') -Force | Out-Null |
| 170 | New-Item -ItemType Directory -Path (Join-Path $script:TempRoot 'docs/guide') -Force | Out-Null |
| 171 | New-Item -ItemType Directory -Path (Join-Path $script:TempRoot 'src') -Force | Out-Null |
| 172 | } |
| 173 | |
| 174 | AfterAll { |
| 175 | Remove-Item -Path $script:TempRoot -Recurse -Force -ErrorAction SilentlyContinue |
| 176 | } |
| 177 | |
| 178 | Context 'Nested directory traversal' { |
| 179 | It 'Returns relative prefix from subdirectory to root' { |
| 180 | $fromPath = Join-Path $script:TempRoot 'docs/guide' |
| 181 | $result = Get-RelativePrefix -FromPath $fromPath -ToPath $script:TempRoot |
| 182 | $result | Should -Be '../../' |
| 183 | } |
| 184 | |
| 185 | It 'Returns relative prefix from single-level directory to root' { |
| 186 | $fromPath = Join-Path $script:TempRoot 'docs' |
| 187 | $result = Get-RelativePrefix -FromPath $fromPath -ToPath $script:TempRoot |
| 188 | $result | Should -Be '../' |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | Context 'Same directory' { |
| 193 | It 'Returns empty string for same directory' { |
| 194 | $result = Get-RelativePrefix -FromPath $script:TempRoot -ToPath $script:TempRoot |
| 195 | $result | Should -Be '' |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | Context 'Sibling directories' { |
| 200 | It 'Returns correct prefix between sibling directories' { |
| 201 | $fromPath = Join-Path $script:TempRoot 'docs' |
| 202 | $toPath = Join-Path $script:TempRoot 'src' |
| 203 | $result = Get-RelativePrefix -FromPath $fromPath -ToPath $toPath |
| 204 | $result | Should -Be '../src/' |
| 205 | } |
| 206 | } |
| 207 | |
| 208 | Context 'Forward slash normalization' { |
| 209 | It 'Returns forward slashes on Windows' { |
| 210 | $fromPath = Join-Path $script:TempRoot 'docs/guide' |
| 211 | $result = Get-RelativePrefix -FromPath $fromPath -ToPath $script:TempRoot |
| 212 | $result | Should -Not -Match '\\' |
| 213 | } |
| 214 | |
| 215 | It 'Always has trailing slash when not empty' { |
| 216 | $fromPath = Join-Path $script:TempRoot 'docs' |
| 217 | $result = Get-RelativePrefix -FromPath $fromPath -ToPath $script:TempRoot |
| 218 | if ($result -ne '') { |
| 219 | $result | Should -Match '/$' |
| 220 | } |
| 221 | } |
| 222 | } |
| 223 | } |
| 224 | |
| 225 | #endregion |
| 226 | |
| 227 | #region Script Integration Tests |
| 228 | |
| 229 | Describe 'Markdown-Link-Check Integration' -Tag 'Integration' { |
| 230 | Context 'Config file loading' { |
| 231 | BeforeAll { |
| 232 | $script:ConfigPath = Join-Path $PSScriptRoot '../Fixtures/Linting/link-check-config.json' |
| 233 | } |
| 234 | |
| 235 | It 'Config fixture file exists' { |
| 236 | Test-Path $script:ConfigPath | Should -BeTrue |
| 237 | } |
| 238 | |
| 239 | It 'Config fixture is valid JSON' { |
| 240 | { Get-Content $script:ConfigPath | ConvertFrom-Json } | Should -Not -Throw |
| 241 | } |
| 242 | |
| 243 | It 'Config contains expected properties' { |
| 244 | $config = Get-Content $script:ConfigPath | ConvertFrom-Json |
| 245 | $config.PSObject.Properties.Name | Should -Contain 'ignorePatterns' |
| 246 | $config.PSObject.Properties.Name | Should -Contain 'replacementPatterns' |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | Context 'Main execution error handling' { |
| 251 | BeforeAll { |
| 252 | $script:OriginalGHA = $env:GITHUB_ACTIONS |
| 253 | $script:LinkCheckScript = Join-Path $PSScriptRoot '../../linting/Markdown-Link-Check.ps1' |
| 254 | } |
| 255 | |
| 256 | AfterAll { |
| 257 | if ($null -eq $script:OriginalGHA) { |
| 258 | Remove-Item Env:GITHUB_ACTIONS -ErrorAction SilentlyContinue |
| 259 | } else { |
| 260 | $env:GITHUB_ACTIONS = $script:OriginalGHA |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | It 'Outputs GitHub error annotation when script fails in CI' { |
| 265 | # Arrange |
| 266 | $env:GITHUB_ACTIONS = 'true' |
| 267 | |
| 268 | # Create temp directory with no markdown files |
| 269 | $emptyDir = Join-Path $TestDrive 'empty-no-md' |
| 270 | New-Item -ItemType Directory -Path $emptyDir -Force | Out-Null |
| 271 | |
| 272 | # Mock git to simulate no tracked markdown files |
| 273 | Mock git { |
| 274 | if ($args -contains 'rev-parse') { |
| 275 | $global:LASTEXITCODE = 0 |
| 276 | return $emptyDir |
| 277 | } |
| 278 | elseif ($args -contains 'ls-files') { |
| 279 | $global:LASTEXITCODE = 0 |
| 280 | return @() # No markdown files |
| 281 | } |
| 282 | } |
| 283 | |
| 284 | # Act - Run script with empty directory (will fail with no files found) |
| 285 | $output = & $script:LinkCheckScript -Path $emptyDir 2>&1 |
| 286 | |
| 287 | # Assert - Should output error |
| 288 | $errors = $output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } |
| 289 | $errors | Should -Not -BeNullOrEmpty |
| 290 | } |
| 291 | } |
| 292 | } |
| 293 | |
| 294 | #endregion |
| 295 | |
| 296 | #region Invoke-MarkdownLinkCheck Tests |
| 297 | |
| 298 | Describe 'Invoke-MarkdownLinkCheck' -Tag 'Unit' { |
| 299 | BeforeAll { |
| 300 | $script:RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '../../..')).Path |
| 301 | $script:FixtureConfig = Join-Path $PSScriptRoot '../Fixtures/Linting/link-check-config.json' |
| 302 | } |
| 303 | |
| 304 | Context 'No markdown files found' { |
| 305 | It 'Throws when Get-MarkdownTarget returns empty' { |
| 306 | Mock Get-MarkdownTarget { return @() } |
| 307 | Mock Resolve-Path { return [PSCustomObject]@{ Path = $script:RepoRoot } } |
| 308 | |
| 309 | { Invoke-MarkdownLinkCheck -Path @('nonexistent') -ConfigPath $script:FixtureConfig } | |
| 310 | Should -Throw '*No markdown files were found to validate*' |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | Context 'CLI not installed' { |
| 315 | It 'Throws when markdown-link-check binary is missing' { |
| 316 | Mock Get-MarkdownTarget { return @('file.md') } |
| 317 | Mock Resolve-Path { return [PSCustomObject]@{ Path = $script:RepoRoot } } |
| 318 | Mock Test-Path { return $false } -ParameterFilter { $LiteralPath -and $LiteralPath -like '*markdown-link-check*' } |
| 319 | |
| 320 | { Invoke-MarkdownLinkCheck -Path @('file.md') -ConfigPath $script:FixtureConfig } | |
| 321 | Should -Throw '*markdown-link-check is not installed*' |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | Context 'Quiet mode base arguments' { |
| 326 | It 'Passes -q flag when Quiet switch is set' { |
| 327 | Mock Get-MarkdownTarget { return @('file.md') } |
| 328 | Mock Resolve-Path { return [PSCustomObject]@{ Path = $script:RepoRoot } } |
| 329 | Mock Test-Path { return $true } -ParameterFilter { $LiteralPath -and $LiteralPath -like '*markdown-link-check*' } |
| 330 | Mock Push-Location { } |
| 331 | Mock Pop-Location { } |
| 332 | Mock Resolve-Path { return [PSCustomObject]@{ Path = "$TestDrive/file.md" } } -ParameterFilter { $LiteralPath -eq 'file.md' } |
| 333 | Mock New-Item { } -ParameterFilter { $ItemType -eq 'Directory' } |
| 334 | Mock Set-Content { } |
| 335 | Mock Write-Host { } |
| 336 | |
| 337 | try { |
| 338 | Invoke-MarkdownLinkCheck -Path @('file.md') -ConfigPath $script:FixtureConfig -Quiet |
| 339 | } |
| 340 | catch { |
| 341 | Write-Verbose "CLI execution expected to fail in test environment: $_" |
| 342 | } |
| 343 | |
| 344 | Should -Invoke Get-MarkdownTarget -Times 1 |
| 345 | Should -Invoke Push-Location -Times 1 |
| 346 | } |
| 347 | } |
| 348 | } |
| 349 | |
| 350 | #endregion |
| 351 | |