microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8b197250063fc1629244f661f78baf9022cebbb0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/tests/security/Test-ActionVersionConsistency.Tests.ps1

770lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4
5using module ../../security/Modules/SecurityClasses.psm1
6
7<#
8.SYNOPSIS
9 Pester tests for Test-ActionVersionConsistency.ps1 functions.
10
11.DESCRIPTION
12 Tests version consistency checking functions without executing the main script.
13 Uses dot-source guard pattern for function isolation.
14#>
15
16BeforeAll {
17 $scriptPath = Join-Path $PSScriptRoot '../../security/Test-ActionVersionConsistency.ps1'
18 . $scriptPath
19
20 $mockPath = Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1'
21 Import-Module $mockPath -Force
22
23 Save-CIEnvironment
24
25 $script:FixturesPath = Join-Path $PSScriptRoot '../Fixtures/Workflows'
26}
27
28AfterAll {
29 Restore-CIEnvironment
30 Remove-Module CIHelpers -Force -ErrorAction SilentlyContinue
31}
32
33Describe 'Write-ConsistencyLog' -Tag 'Unit' {
34 Context 'Log output' {
35 It 'Does not throw for Info level' {
36 { Write-ConsistencyLog -Message 'Test message' -Level Info } | Should -Not -Throw
37 }
38
39 It 'Does not throw for Warning level' {
40 { Write-ConsistencyLog -Message 'Warning message' -Level Warning } | Should -Not -Throw
41 }
42
43 It 'Does not throw for Error level' {
44 { Write-ConsistencyLog -Message 'Error message' -Level Error } | Should -Not -Throw
45 }
46
47 It 'Does not throw for Success level' {
48 { Write-ConsistencyLog -Message 'Success message' -Level Success } | Should -Not -Throw
49 }
50
51 It 'Defaults to Info level when not specified' {
52 { Write-ConsistencyLog -Message 'Default level test' } | Should -Not -Throw
53 }
54 }
55
56 Context 'CI annotation forwarding' {
57 BeforeAll {
58 Mock Write-CIAnnotation {}
59 Mock Write-Host {}
60 }
61
62 It 'Forwards Warning level to Write-CIAnnotation' {
63 Write-ConsistencyLog -Message 'warn msg' -Level Warning
64 Should -Invoke Write-CIAnnotation -Times 1 -Exactly -ParameterFilter {
65 $Message -eq 'warn msg' -and $Level -eq 'Warning'
66 }
67 }
68
69 It 'Forwards Error level to Write-CIAnnotation' {
70 Write-ConsistencyLog -Message 'err msg' -Level Error
71 Should -Invoke Write-CIAnnotation -Times 1 -Exactly -ParameterFilter {
72 $Message -eq 'err msg' -and $Level -eq 'Error'
73 }
74 }
75
76 It 'Does not forward Info or Success levels to Write-CIAnnotation' {
77 Write-ConsistencyLog -Message 'info msg' -Level Info
78 Write-ConsistencyLog -Message 'success msg' -Level Success
79 Should -Invoke Write-CIAnnotation -Times 0 -Exactly
80 }
81 }
82}
83
84Describe 'Get-ActionVersionViolations' -Tag 'Unit' {
85 Context 'Clean workflow (no violations)' {
86 It 'Returns zero violations for fully commented workflow' {
87 $testPath = Join-Path $TestDrive 'clean-workflow-test'
88 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
89 Copy-Item -Path (Join-Path $script:FixturesPath 'consistent-versions.yml') -Destination $testPath
90
91 $result = Get-ActionVersionViolations -WorkflowPath $testPath
92 $result.Violations | Should -BeNullOrEmpty
93 }
94
95 It 'Returns correct TotalActions count' {
96 $testPath = Join-Path $TestDrive 'consistent-test'
97 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
98 Copy-Item -Path (Join-Path $script:FixturesPath 'consistent-versions.yml') -Destination $testPath
99
100 $result = Get-ActionVersionViolations -WorkflowPath $testPath
101 $result.TotalActions | Should -Be 3
102 }
103
104 It 'Returns empty violations array for pinned-workflow.yml' {
105 $testPath = Join-Path $TestDrive 'pinned-test'
106 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
107 Copy-Item -Path (Join-Path $script:FixturesPath 'pinned-workflow.yml') -Destination $testPath
108
109 $result = Get-ActionVersionViolations -WorkflowPath $testPath
110 $result.Violations.Count | Should -Be 0
111 }
112 }
113
114 Context 'Missing version comment (single)' {
115 It 'Detects SHA without version comment' {
116 $testPath = Join-Path $TestDrive 'missing-single'
117 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
118 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
119
120 $result = Get-ActionVersionViolations -WorkflowPath $testPath
121 $missingComments = $result.Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' }
122 $missingComments.Count | Should -Be 1
123 }
124
125 It 'Returns correct violation properties for missing comment' {
126 $testPath = Join-Path $TestDrive 'missing-props'
127 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
128 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
129
130 $result = Get-ActionVersionViolations -WorkflowPath $testPath
131 $violation = $result.Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' } | Select-Object -First 1
132
133 $violation.Type | Should -Be 'github-actions'
134 $violation.Severity | Should -Be 'Medium'
135 $violation.Name | Should -Be 'actions/checkout'
136 $violation.Description | Should -Match 'missing version comment'
137 }
138
139 It 'Includes FullSha in violation Metadata' {
140 $testPath = Join-Path $TestDrive 'missing-meta'
141 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
142 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
143
144 $result = Get-ActionVersionViolations -WorkflowPath $testPath
145 $violation = $result.Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' } | Select-Object -First 1
146
147 $violation.Metadata.FullSha | Should -Be 'a5ac7e51b41094c92402da3b24376905380afc29'
148 }
149 }
150
151 Context 'Missing version comments (multiple)' {
152 It 'Detects all missing version comments' {
153 $testPath = Join-Path $TestDrive 'missing-multiple'
154 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
155 Copy-Item -Path (Join-Path $script:FixturesPath 'multiple-missing-comments.yml') -Destination $testPath
156
157 $result = Get-ActionVersionViolations -WorkflowPath $testPath
158 $missingComments = $result.Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' }
159 $missingComments.Count | Should -Be 3
160 }
161
162 It 'Returns unique line numbers for each violation' {
163 $testPath = Join-Path $TestDrive 'missing-lines'
164 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
165 Copy-Item -Path (Join-Path $script:FixturesPath 'multiple-missing-comments.yml') -Destination $testPath
166
167 $result = Get-ActionVersionViolations -WorkflowPath $testPath
168 $lines = $result.Violations | ForEach-Object { $_.Line } | Sort-Object -Unique
169 $lines.Count | Should -Be $result.Violations.Count
170 }
171 }
172
173 Context 'Version mismatch detection' {
174 It 'Detects version mismatch for same SHA across files' {
175 $testPath = Join-Path $TestDrive 'mismatch-test'
176 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
177 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
178 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
179
180 $result = Get-ActionVersionViolations -WorkflowPath $testPath
181 $mismatches = $result.Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' }
182 $mismatches.Count | Should -Be 1
183 }
184
185 It 'Returns High severity for version mismatch' {
186 $testPath = Join-Path $TestDrive 'mismatch-severity'
187 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
188 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
189 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
190
191 $result = Get-ActionVersionViolations -WorkflowPath $testPath
192 $mismatch = $result.Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' } | Select-Object -First 1
193 $mismatch.Severity | Should -Be 'High'
194 }
195
196 It 'Includes conflicting versions in Metadata' {
197 $testPath = Join-Path $TestDrive 'mismatch-meta'
198 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
199 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
200 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
201
202 $result = Get-ActionVersionViolations -WorkflowPath $testPath
203 $mismatch = $result.Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' } | Select-Object -First 1
204 $mismatch.Metadata.ConflictingVersions | Should -Match 'v4\.1\.0'
205 $mismatch.Metadata.ConflictingVersions | Should -Match 'v4\.1\.6'
206 }
207
208 It 'Includes affected locations in Metadata' {
209 $testPath = Join-Path $TestDrive 'mismatch-locations'
210 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
211 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
212 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
213
214 $result = Get-ActionVersionViolations -WorkflowPath $testPath
215 $mismatch = $result.Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' } | Select-Object -First 1
216 $mismatch.Metadata.AffectedLocations.Count | Should -Be 2
217 }
218 }
219
220 Context 'Non-existent path handling' {
221 BeforeAll {
222 # Use platform-agnostic path that guaranteed doesn't exist
223 $script:NonExistentPath = Join-Path ([System.IO.Path]::GetTempPath()) "nonexistent-$(New-Guid)"
224 }
225
226 It 'Returns empty violations for non-existent path' {
227 $result = Get-ActionVersionViolations -WorkflowPath $script:NonExistentPath
228 $result.Violations | Should -BeNullOrEmpty
229 }
230
231 It 'Returns zero TotalActions for non-existent path' {
232 $result = Get-ActionVersionViolations -WorkflowPath $script:NonExistentPath
233 $result.TotalActions | Should -Be 0
234 }
235
236 It 'Returns empty ShaVersionMap for non-existent path' {
237 $result = Get-ActionVersionViolations -WorkflowPath $script:NonExistentPath
238 $result.ShaVersionMap.Count | Should -Be 0
239 }
240 }
241
242 Context 'Empty directory handling' {
243 It 'Returns empty violations for empty directory' {
244 $emptyPath = Join-Path $TestDrive 'empty-workflows'
245 New-Item -ItemType Directory -Path $emptyPath -Force | Out-Null
246
247 $result = Get-ActionVersionViolations -WorkflowPath $emptyPath
248 $result.Violations | Should -BeNullOrEmpty
249 }
250
251 It 'Returns zero TotalActions for empty directory' {
252 $emptyPath = Join-Path $TestDrive 'empty-workflows-count'
253 New-Item -ItemType Directory -Path $emptyPath -Force | Out-Null
254
255 $result = Get-ActionVersionViolations -WorkflowPath $emptyPath
256 $result.TotalActions | Should -Be 0
257 }
258 }
259
260 Context 'Mixed violations (both types in one scan)' {
261 It 'Detects both mismatch and missing comment violations' {
262 $testPath = Join-Path $TestDrive 'mixed-violations'
263 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
264 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
265 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
266 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
267
268 $result = Get-ActionVersionViolations -WorkflowPath $testPath
269 $mismatches = @($result.Violations | Where-Object { $_.ViolationType -eq 'VersionMismatch' })
270 $missingComments = @($result.Violations | Where-Object { $_.ViolationType -eq 'MissingVersionComment' })
271
272 $mismatches.Count | Should -BeGreaterThan 0
273 $missingComments.Count | Should -BeGreaterThan 0
274 }
275 }
276}
277
278Describe 'Export-ConsistencyReport' -Tag 'Unit' {
279 BeforeEach {
280 $script:TestOutputPath = Join-Path $TestDrive 'report'
281 New-Item -ItemType Directory -Path $script:TestOutputPath -Force | Out-Null
282
283 # Create mock violations
284 $script:MockViolations = @()
285
286 $v1 = [DependencyViolation]::new()
287 $v1.File = 'workflow.yml'
288 $v1.Line = 10
289 $v1.Type = 'github-actions'
290 $v1.Name = 'actions/checkout'
291 $v1.Version = 'a5ac7e5'
292 $v1.Severity = 'Medium'
293 $v1.ViolationType = 'MissingVersionComment'
294 $v1.Description = 'SHA-pinned action missing version comment'
295 $v1.Remediation = 'Add version comment'
296 $v1.Metadata = @{ FullSha = 'a5ac7e51b41094c92402da3b24376905380afc29' }
297 $script:MockViolations += $v1
298
299 $v2 = [DependencyViolation]::new()
300 $v2.File = 'other.yml'
301 $v2.Line = 15
302 $v2.Type = 'github-actions'
303 $v2.Name = 'actions/setup-node'
304 $v2.Version = '60edb5d'
305 $v2.Severity = 'High'
306 $v2.ViolationType = 'VersionMismatch'
307 $v2.Description = 'Same SHA has conflicting version comments'
308 $v2.Remediation = 'Standardize version comment'
309 $v2.Metadata = @{ ConflictingVersions = 'v4.0.0, v4.0.2' }
310 $script:MockViolations += $v2
311 }
312
313 Context 'Table format output' {
314 It 'Does not throw for Table format' {
315 { Export-ConsistencyReport -Violations $script:MockViolations -Format Table -TotalActions 5 } | Should -Not -Throw
316 }
317
318 It 'Writes file when OutputPath specified' {
319 $outputFile = Join-Path $script:TestOutputPath 'report.txt'
320 Export-ConsistencyReport -Violations $script:MockViolations -Format Table -OutputPath $outputFile -TotalActions 5
321 Test-Path $outputFile | Should -BeTrue
322 }
323
324 It 'Reports success message when no violations' {
325 # Capture Host output
326 $output = Export-ConsistencyReport -Violations @() -Format Table -TotalActions 0 6>&1
327 ($output -join ' ') | Should -Match 'No version consistency violations found'
328 }
329 }
330
331 Context 'JSON format with OutputPath' {
332 It 'Generates valid JSON' {
333 $outputFile = Join-Path $script:TestOutputPath 'report.json'
334 Export-ConsistencyReport -Violations $script:MockViolations -Format Json -OutputPath $outputFile -TotalActions 5
335
336 Test-Path $outputFile | Should -BeTrue
337 $content = Get-Content $outputFile -Raw
338 { $content | ConvertFrom-Json } | Should -Not -Throw
339 }
340
341 It 'Includes Timestamp field' {
342 $outputFile = Join-Path $script:TestOutputPath 'report-ts.json'
343 Export-ConsistencyReport -Violations $script:MockViolations -Format Json -OutputPath $outputFile -TotalActions 5
344
345 $rawContent = Get-Content $outputFile -Raw
346 $rawContent | Should -Match '"Timestamp":\s*"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}'
347 }
348
349 It 'Includes TotalActions count' {
350 $outputFile = Join-Path $script:TestOutputPath 'report-total.json'
351 Export-ConsistencyReport -Violations $script:MockViolations -Format Json -OutputPath $outputFile -TotalActions 10
352
353 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
354 $content.TotalActions | Should -Be 10
355 }
356
357 It 'Includes MismatchCount' {
358 $outputFile = Join-Path $script:TestOutputPath 'report-mismatch.json'
359 Export-ConsistencyReport -Violations $script:MockViolations -Format Json -OutputPath $outputFile -TotalActions 5
360
361 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
362 $content.MismatchCount | Should -Be 1
363 }
364
365 It 'Includes MissingComments count' {
366 $outputFile = Join-Path $script:TestOutputPath 'report-missing.json'
367 Export-ConsistencyReport -Violations $script:MockViolations -Format Json -OutputPath $outputFile -TotalActions 5
368
369 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
370 $content.MissingComments | Should -Be 1
371 }
372
373 It 'Includes Violations array' {
374 $outputFile = Join-Path $script:TestOutputPath 'report-violations.json'
375 Export-ConsistencyReport -Violations $script:MockViolations -Format Json -OutputPath $outputFile -TotalActions 5
376
377 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
378 $content.Violations.Count | Should -Be 2
379 }
380 }
381
382 Context 'SARIF format schema validation' {
383 It 'Generates valid SARIF structure' {
384 $outputFile = Join-Path $script:TestOutputPath 'report.sarif'
385 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
386
387 Test-Path $outputFile | Should -BeTrue
388 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
389 $content.version | Should -Be '2.1.0'
390 }
391
392 It 'Includes schema reference' {
393 $outputFile = Join-Path $script:TestOutputPath 'report-schema.sarif'
394 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
395
396 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
397 $content.'$schema' | Should -Match 'sarif-2\.1\.0\.json'
398 }
399
400 It 'Includes tool driver information' {
401 $outputFile = Join-Path $script:TestOutputPath 'report-tool.sarif'
402 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
403
404 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
405 $content.runs[0].tool.driver.name | Should -Be 'action-version-consistency'
406 }
407
408 It 'Maps violations to SARIF results' {
409 $outputFile = Join-Path $script:TestOutputPath 'report-results.sarif'
410 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
411
412 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
413 $content.runs[0].results.Count | Should -Be 2
414 }
415
416 It 'Maps MissingVersionComment to warning level' {
417 $outputFile = Join-Path $script:TestOutputPath 'report-warning.sarif'
418 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
419
420 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
421 $warningResult = $content.runs[0].results | Where-Object { $_.ruleId -eq 'missing-version-comment' }
422 $warningResult.level | Should -Be 'warning'
423 }
424
425 It 'Maps VersionMismatch to error level' {
426 $outputFile = Join-Path $script:TestOutputPath 'report-error.sarif'
427 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
428
429 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
430 $errorResult = $content.runs[0].results | Where-Object { $_.ruleId -eq 'version-mismatch' }
431 $errorResult.level | Should -Be 'error'
432 }
433
434 It 'Includes locations with file and line' {
435 $outputFile = Join-Path $script:TestOutputPath 'report-locations.sarif'
436 Export-ConsistencyReport -Violations $script:MockViolations -Format Sarif -OutputPath $outputFile -TotalActions 5
437
438 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
439 $result = $content.runs[0].results[0]
440 $result.locations[0].physicalLocation.artifactLocation.uri | Should -Not -BeNullOrEmpty
441 $result.locations[0].physicalLocation.region.startLine | Should -BeGreaterThan 0
442 }
443 }
444
445 Context 'Empty violations array' {
446 It 'Handles empty violations for JSON format' {
447 $outputFile = Join-Path $script:TestOutputPath 'empty.json'
448 Export-ConsistencyReport -Violations @() -Format Json -OutputPath $outputFile -TotalActions 0
449
450 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
451 $content.Violations.Count | Should -Be 0
452 $content.MismatchCount | Should -Be 0
453 $content.MissingComments | Should -Be 0
454 }
455
456 It 'Handles empty violations for SARIF format' {
457 $outputFile = Join-Path $script:TestOutputPath 'empty.sarif'
458 Export-ConsistencyReport -Violations @() -Format Sarif -OutputPath $outputFile -TotalActions 0
459
460 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
461 $content.runs[0].results.Count | Should -Be 0
462 }
463 }
464
465 Context 'Multiple violations of each type' {
466 BeforeEach {
467 $script:MultipleViolations = @()
468
469 # Add 3 MissingVersionComment violations
470 for ($i = 1; $i -le 3; $i++) {
471 $v = [DependencyViolation]::new()
472 $v.File = "workflow$i.yml"
473 $v.Line = $i * 10
474 $v.Type = 'github-actions'
475 $v.Name = "actions/action$i"
476 $v.Version = "sha$i"
477 $v.Severity = 'Medium'
478 $v.ViolationType = 'MissingVersionComment'
479 $v.Description = 'Missing comment'
480 $script:MultipleViolations += $v
481 }
482
483 # Add 2 VersionMismatch violations
484 for ($i = 1; $i -le 2; $i++) {
485 $v = [DependencyViolation]::new()
486 $v.File = "mismatch$i.yml"
487 $v.Line = $i * 5
488 $v.Type = 'github-actions'
489 $v.Name = "actions/mismatch$i"
490 $v.Version = "msha$i"
491 $v.Severity = 'High'
492 $v.ViolationType = 'VersionMismatch'
493 $v.Description = 'Version mismatch'
494 $script:MultipleViolations += $v
495 }
496 }
497
498 It 'Counts multiple MissingVersionComment violations correctly' {
499 $outputFile = Join-Path $script:TestOutputPath 'multiple.json'
500 Export-ConsistencyReport -Violations $script:MultipleViolations -Format Json -OutputPath $outputFile -TotalActions 10
501
502 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
503 $content.MissingComments | Should -Be 3
504 }
505
506 It 'Counts multiple VersionMismatch violations correctly' {
507 $outputFile = Join-Path $script:TestOutputPath 'multiple-mismatch.json'
508 Export-ConsistencyReport -Violations $script:MultipleViolations -Format Json -OutputPath $outputFile -TotalActions 10
509
510 $content = Get-Content $outputFile -Raw | ConvertFrom-Json
511 $content.MismatchCount | Should -Be 2
512 }
513 }
514}
515
516Describe 'Invoke-ActionVersionConsistency' -Tag 'Unit' {
517 Context 'CI annotations per violation' {
518 BeforeEach {
519 $script:mockFiles = Initialize-MockCIEnvironment
520 }
521
522 AfterEach {
523 Remove-MockCIFiles -MockFiles $script:mockFiles
524 Restore-CIEnvironment
525 }
526
527 It 'Emits Warning annotation for missing version comment' {
528 $testPath = Join-Path $TestDrive 'annotate-missing'
529 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
530 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
531
532 Mock Write-CIAnnotation {}
533
534 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
535
536 Should -Invoke Write-CIAnnotation -ParameterFilter { $Level -eq 'Warning' -and $null -ne $File } -Times 1 -Exactly
537 }
538
539 It 'Emits Error annotation for version mismatch' {
540 $testPath = Join-Path $TestDrive 'annotate-mismatch'
541 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
542 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
543 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
544
545 Mock Write-CIAnnotation {}
546
547 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
548
549 Should -Invoke Write-CIAnnotation -ParameterFilter { $Level -eq 'Error' } -Times 1 -Exactly
550 }
551
552 It 'Emits no annotations when no violations exist' {
553 $testPath = Join-Path $TestDrive 'annotate-clean'
554 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
555 Copy-Item -Path (Join-Path $script:FixturesPath 'consistent-versions.yml') -Destination $testPath
556
557 Mock Write-CIAnnotation {}
558
559 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
560
561 Should -Invoke Write-CIAnnotation -Times 0
562 }
563
564 It 'Includes file and line in annotation' {
565 $testPath = Join-Path $TestDrive 'annotate-location'
566 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
567 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
568
569 Mock Write-CIAnnotation {}
570
571 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
572
573 Should -Invoke Write-CIAnnotation -ParameterFilter { $File -and $Line -gt 0 }
574 }
575
576 It 'Includes violation type in annotation message' {
577 $testPath = Join-Path $TestDrive 'annotate-message'
578 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
579 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
580
581 Mock Write-CIAnnotation {}
582
583 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
584
585 Should -Invoke Write-CIAnnotation -ParameterFilter { $Message -match 'MissingVersionComment' }
586 }
587 }
588
589 Context 'CI step summary' {
590 BeforeEach {
591 $script:mockFiles = Initialize-MockCIEnvironment
592 }
593
594 AfterEach {
595 Remove-MockCIFiles -MockFiles $script:mockFiles
596 Restore-CIEnvironment
597 }
598
599 It 'Writes passed status when no violations' {
600 $testPath = Join-Path $TestDrive 'summary-clean'
601 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
602 Copy-Item -Path (Join-Path $script:FixturesPath 'consistent-versions.yml') -Destination $testPath
603
604 Mock Write-CIAnnotation {}
605
606 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
607
608 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
609 $summaryContent | Should -Match 'Passed'
610 }
611
612 It 'Writes failed status when violations exist' {
613 $testPath = Join-Path $TestDrive 'summary-fail'
614 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
615 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
616
617 Mock Write-CIAnnotation {}
618
619 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
620
621 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
622 $summaryContent | Should -Match 'Failed'
623 }
624
625 It 'Includes violation counts in summary table' {
626 $testPath = Join-Path $TestDrive 'summary-counts'
627 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
628 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
629
630 Mock Write-CIAnnotation {}
631
632 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
633
634 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
635 $summaryContent | Should -Match 'Missing Comments'
636 $summaryContent | Should -Match 'Version Mismatches'
637 }
638
639 It 'Includes action version consistency heading' {
640 $testPath = Join-Path $TestDrive 'summary-heading'
641 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
642 Copy-Item -Path (Join-Path $script:FixturesPath 'consistent-versions.yml') -Destination $testPath
643
644 Mock Write-CIAnnotation {}
645
646 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
647
648 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
649 $summaryContent | Should -Match 'Action Version Consistency'
650 }
651
652 It 'Lists individual violation file and action in summary table' {
653 $testPath = Join-Path $TestDrive 'summary-detail'
654 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
655 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
656
657 Mock Write-CIAnnotation {}
658
659 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
660
661 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
662 $summaryContent | Should -Match 'actions/checkout'
663 $summaryContent | Should -Match 'MissingVersionComment'
664 }
665
666 It 'Includes violations table header row' {
667 $testPath = Join-Path $TestDrive 'summary-table-header'
668 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
669 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
670
671 Mock Write-CIAnnotation {}
672
673 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
674
675 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
676 $summaryContent | Should -Match '\| File \| Line \| Type \| Action \| Severity \| Description \|'
677 }
678
679 It 'Lists each violation as a separate row for mixed violation types' {
680 $testPath = Join-Path $TestDrive 'summary-mixed'
681 New-Item -ItemType Directory -Path $testPath -Force | Out-Null
682 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $testPath
683 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $testPath
684 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $testPath
685
686 Mock Write-CIAnnotation {}
687
688 $null = Invoke-ActionVersionConsistency -Path $testPath -Format Table
689
690 $summaryContent = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
691 $summaryContent | Should -Match 'VersionMismatch'
692 $summaryContent | Should -Match 'MissingVersionComment'
693 # At least 2 data rows (pipe-delimited lines after the header separator)
694 $lines = ($summaryContent -split "`n") | ForEach-Object { $_.TrimEnd("`r") }
695 $dataRows = $lines | Where-Object { $_ -match '^\|.*\|$' -and $_ -notmatch '^\| File' -and $_ -notmatch '^\|[-\s|]+\|$' }
696 $dataRows.Count | Should -BeGreaterOrEqual 2
697 }
698 }
699}
700
701Describe 'Main Script Execution' -Tag 'Unit' {
702 BeforeAll {
703 $script:TestScript = (Resolve-Path (Join-Path $PSScriptRoot '../../security/Test-ActionVersionConsistency.ps1')).Path
704 $script:FixturesPath = Join-Path $PSScriptRoot '../Fixtures/Workflows'
705 # Use cross-platform temp directory (accessible from child process, unlike $TestDrive)
706 $tempBase = [System.IO.Path]::GetTempPath()
707 $script:MainTestRoot = Join-Path $tempBase "pester-main-$(Get-Random)"
708 New-Item -ItemType Directory -Path $script:MainTestRoot -Force | Out-Null
709 }
710
711 AfterAll {
712 if ($script:MainTestRoot -and (Test-Path $script:MainTestRoot)) {
713 Remove-Item -Path $script:MainTestRoot -Recurse -Force -ErrorAction SilentlyContinue
714 }
715 }
716
717 Context 'Exit code handling' {
718 BeforeEach {
719 $script:TestWorkspace = Join-Path $script:MainTestRoot "test-$(Get-Random)"
720 New-Item -ItemType Directory -Path $script:TestWorkspace -Force | Out-Null
721 }
722
723 AfterEach {
724 if ($script:TestWorkspace -and (Test-Path $script:TestWorkspace)) {
725 Remove-Item -Path $script:TestWorkspace -Recurse -Force -ErrorAction SilentlyContinue
726 }
727 }
728
729 It 'Returns exit code 0 when no violations and no fail flags' {
730 Copy-Item -Path (Join-Path $script:FixturesPath 'pinned-workflow.yml') -Destination $script:TestWorkspace
731
732 $null = pwsh -NoProfile -Command "& '$script:TestScript' -Path '$script:TestWorkspace' -Format Json" 2>&1
733 $LASTEXITCODE | Should -Be 0
734 }
735
736 It 'Returns exit code 1 when FailOnMismatch and mismatches exist' {
737 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-a.yml') -Destination $script:TestWorkspace
738 Copy-Item -Path (Join-Path $script:FixturesPath 'version-mismatch-b.yml') -Destination $script:TestWorkspace
739
740 $tempScript = Join-Path $script:TestWorkspace 'run-test.ps1'
741 $scriptContent = @"
742& '$($script:TestScript)' -Path '$($script:TestWorkspace)' -Format Json -FailOnMismatch
743exit `$LASTEXITCODE
744"@
745 Set-Content -Path $tempScript -Value $scriptContent
746 $proc = Start-Process -FilePath 'pwsh' -ArgumentList @('-NoProfile', '-File', $tempScript) -Wait -PassThru -NoNewWindow
747 $proc.ExitCode | Should -Be 1
748 }
749
750 It 'Returns exit code 1 when FailOnMissingComment and missing comments exist' {
751 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $script:TestWorkspace
752
753 $tempScript = Join-Path $script:TestWorkspace 'run-test.ps1'
754 $scriptContent = @"
755& '$($script:TestScript)' -Path '$($script:TestWorkspace)' -Format Json -FailOnMissingComment
756exit `$LASTEXITCODE
757"@
758 Set-Content -Path $tempScript -Value $scriptContent
759 $proc = Start-Process -FilePath 'pwsh' -ArgumentList @('-NoProfile', '-File', $tempScript) -Wait -PassThru -NoNewWindow
760 $proc.ExitCode | Should -Be 1
761 }
762
763 It 'Returns exit code 0 when violations exist but no fail flags set' {
764 Copy-Item -Path (Join-Path $script:FixturesPath 'missing-version-comment.yml') -Destination $script:TestWorkspace
765
766 $null = pwsh -NoProfile -Command "& '$script:TestScript' -Path '$script:TestWorkspace' -Format Json" 2>&1
767 $LASTEXITCODE | Should -Be 0
768 }
769 }
770}
771