microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/context-working

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/lib/CIHelpers.Tests.ps1

870lines · modecode

1# Copyright (c) Microsoft Corporation.
2# SPDX-License-Identifier: MIT
3
4#Requires -Modules Pester
5# CIHelpers.Tests.ps1
6#
7# Purpose: Unit tests for CIHelpers.psm1 module
8# Author: HVE Core Team
9
10BeforeAll {
11 $modulePath = Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1'
12 Import-Module $modulePath -Force
13
14 $mockPath = Join-Path $PSScriptRoot '../Mocks/GitMocks.psm1'
15 Import-Module $mockPath -Force
16}
17
18Describe 'Get-StandardTimestamp' -Tag 'Unit' {
19 It 'Returns a string' {
20 Get-StandardTimestamp | Should -BeOfType [string]
21 }
22
23 It 'Returns a non-empty value' {
24 Get-StandardTimestamp | Should -Not -BeNullOrEmpty
25 }
26
27 It 'Matches ISO 8601 UTC format ending in Z' {
28 Get-StandardTimestamp | Should -Match '^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$'
29 }
30
31 It 'Returns monotonically increasing timestamps on consecutive calls' {
32 $earlier = [datetime]::new(2025, 1, 15, 18, 30, 0, [System.DateTimeKind]::Utc)
33 $later = $earlier.AddSeconds(1)
34
35 Mock Get-Date { $earlier } -ModuleName CIHelpers
36 $first = [datetime]::Parse((Get-StandardTimestamp))
37
38 Mock Get-Date { $later } -ModuleName CIHelpers
39 $second = [datetime]::Parse((Get-StandardTimestamp))
40
41 $second | Should -BeGreaterThan $first
42 }
43}
44
45Describe 'Get-StandardTimestampPattern' -Tag 'Unit' {
46 It 'Returns a non-empty string' {
47 Get-StandardTimestampPattern | Should -Not -BeNullOrEmpty
48 }
49
50 It 'Matches a valid ISO 8601 UTC timestamp' {
51 '2026-04-16T14:55:00.0000000Z' | Should -Match (Get-StandardTimestampPattern)
52 }
53
54 It 'Does not match a timestamp missing fractional seconds' {
55 '2026-04-16T14:55:00Z' | Should -Not -Match (Get-StandardTimestampPattern)
56 }
57
58 It 'Does not match a local time without Z suffix' {
59 '2026-04-16T14:55:00.0000000+00:00' | Should -Not -Match (Get-StandardTimestampPattern)
60 }
61
62 It 'Pattern matches Get-StandardTimestamp output' {
63 $timestamp = Get-StandardTimestamp
64 $pattern = Get-StandardTimestampPattern
65 $timestamp | Should -Match $pattern
66 }
67}
68
69Describe 'Get-CIPlatform' -Tag 'Unit' {
70 BeforeAll {
71 Save-CIEnvironment
72 }
73
74 AfterAll {
75 Restore-CIEnvironment
76 }
77
78 Context 'In GitHub Actions environment' {
79 BeforeEach {
80 Clear-MockCIEnvironment
81 $env:GITHUB_ACTIONS = 'true'
82 }
83
84 It 'Returns github' {
85 Get-CIPlatform | Should -Be 'github'
86 }
87 }
88
89 Context 'In Azure DevOps environment with TF_BUILD' {
90 BeforeEach {
91 Clear-MockCIEnvironment
92 $env:TF_BUILD = 'True'
93 }
94
95 It 'Returns azdo' {
96 Get-CIPlatform | Should -Be 'azdo'
97 }
98 }
99
100 Context 'In Azure DevOps environment with AZURE_PIPELINES' {
101 BeforeEach {
102 Clear-MockCIEnvironment
103 $env:AZURE_PIPELINES = 'True'
104 }
105
106 It 'Returns azdo' {
107 Get-CIPlatform | Should -Be 'azdo'
108 }
109 }
110
111 Context 'In local environment' {
112 BeforeEach {
113 Clear-MockCIEnvironment
114 }
115
116 It 'Returns local' {
117 Get-CIPlatform | Should -Be 'local'
118 }
119 }
120
121 Context 'GitHub takes priority over Azure DevOps' {
122 BeforeEach {
123 Clear-MockCIEnvironment
124 $env:GITHUB_ACTIONS = 'true'
125 $env:TF_BUILD = 'True'
126 }
127
128 It 'Returns github when both are set' {
129 Get-CIPlatform | Should -Be 'github'
130 }
131 }
132}
133
134Describe 'Test-CIEnvironment' -Tag 'Unit' {
135 BeforeAll {
136 Save-CIEnvironment
137 }
138
139 AfterAll {
140 Restore-CIEnvironment
141 }
142
143 Context 'In GitHub Actions environment' {
144 BeforeEach {
145 Clear-MockCIEnvironment
146 $env:GITHUB_ACTIONS = 'true'
147 }
148
149 It 'Returns true' {
150 Test-CIEnvironment | Should -BeTrue
151 }
152 }
153
154 Context 'In Azure DevOps environment' {
155 BeforeEach {
156 Clear-MockCIEnvironment
157 $env:TF_BUILD = 'True'
158 }
159
160 It 'Returns true' {
161 Test-CIEnvironment | Should -BeTrue
162 }
163 }
164
165 Context 'In local environment' {
166 BeforeEach {
167 Clear-MockCIEnvironment
168 }
169
170 It 'Returns false' {
171 Test-CIEnvironment | Should -BeFalse
172 }
173 }
174}
175
176Describe 'Set-CIOutput' -Tag 'Unit' {
177 BeforeAll {
178 Save-CIEnvironment
179 }
180
181 AfterAll {
182 Restore-CIEnvironment
183 }
184
185 Context 'In GitHub Actions environment' {
186 BeforeEach {
187 $script:mockFiles = Initialize-MockCIEnvironment
188 }
189
190 AfterEach {
191 Remove-MockCIFiles -MockFiles $script:mockFiles
192 }
193
194 It 'Writes output to GITHUB_OUTPUT file' {
195 Set-CIOutput -Name 'test-key' -Value 'test-value'
196 $content = Get-Content -Path $env:GITHUB_OUTPUT -Raw
197 $content | Should -Match 'test-key=test-value'
198 }
199
200 It 'Appends multiple outputs' {
201 Set-CIOutput -Name 'key1' -Value 'value1'
202 Set-CIOutput -Name 'key2' -Value 'value2'
203 $content = Get-Content -Path $env:GITHUB_OUTPUT -Raw
204 $content | Should -Match 'key1=value1'
205 $content | Should -Match 'key2=value2'
206 }
207 }
208
209 Context 'In Azure DevOps environment' {
210 BeforeEach {
211 Clear-MockCIEnvironment
212 $env:TF_BUILD = 'True'
213 }
214
215 It 'Outputs task.setvariable format' {
216 $output = Set-CIOutput -Name 'test-key' -Value 'test-value'
217 $output | Should -Be '##vso[task.setvariable variable=test-key]test-value'
218 }
219
220 It 'Includes isOutput flag when specified' {
221 $output = Set-CIOutput -Name 'test-key' -Value 'test-value' -IsOutput
222 $output | Should -Be '##vso[task.setvariable variable=test-key;isOutput=true]test-value'
223 }
224 }
225
226 Context 'In local environment' {
227 BeforeEach {
228 Clear-MockCIEnvironment
229 }
230
231 It 'Does not produce console output' {
232 $output = Set-CIOutput -Name 'test-key' -Value 'test-value'
233 $output | Should -BeNullOrEmpty
234 }
235 }
236
237 Context 'GitHub with missing GITHUB_OUTPUT' {
238 BeforeEach {
239 Clear-MockCIEnvironment
240 $env:GITHUB_ACTIONS = 'true'
241 }
242
243 It 'Handles missing GITHUB_OUTPUT gracefully' {
244 { Set-CIOutput -Name 'test-key' -Value 'test-value' } | Should -Not -Throw
245 }
246 }
247
248 Context 'Workflow command injection prevention (Azure DevOps)' {
249 BeforeEach {
250 Clear-MockCIEnvironment
251 $env:TF_BUILD = 'True'
252 }
253
254 It 'Escapes newlines in value to prevent command injection' {
255 $maliciousValue = "value`n##vso[task.setvariable variable=pwned]true"
256 $output = Set-CIOutput -Name 'test-key' -Value $maliciousValue
257 $output | Should -Not -Match '##vso\[task\.setvariable variable=pwned\]'
258 $output | Should -Match '%AZP0A'
259 }
260
261 It 'Escapes semicolons in variable name to prevent property injection' {
262 $maliciousName = 'test;isOutput=true'
263 $output = Set-CIOutput -Name $maliciousName -Value 'value'
264 $output | Should -Match '%AZP3B'
265 }
266 }
267}
268
269Describe 'Set-CIEnv' -Tag 'Unit' {
270 BeforeAll {
271 Save-CIEnvironment
272 }
273
274 AfterAll {
275 Restore-CIEnvironment
276 }
277
278 Context 'In GitHub Actions environment' {
279 BeforeEach {
280 $script:mockFiles = Initialize-MockCIEnvironment
281 }
282
283 AfterEach {
284 Remove-MockCIFiles -MockFiles $script:mockFiles
285 }
286
287 It 'Writes environment variable to GITHUB_ENV file' {
288 Set-CIEnv -Name 'TEST_VAR' -Value 'test-value'
289 $content = Get-Content -Path $env:GITHUB_ENV -Raw
290 $content | Should -Match 'TEST_VAR<<EOF_[a-f0-9]+'
291 $content | Should -Match 'test-value'
292 }
293
294 It 'Preserves newlines in environment variable value using delimiter format' {
295 Set-CIEnv -Name 'TEST_VAR' -Value "line1`nline2"
296 $content = Get-Content -Path $env:GITHUB_ENV -Raw
297 $content | Should -Match 'line1'
298 $content | Should -Match 'line2'
299 $content | Should -Not -Match '%0A'
300 }
301
302 It 'Rejects invalid variable names' {
303 { Set-CIEnv -Name 'invalid-name' -Value 'test' } | Should -Throw -ExpectedMessage '*Invalid GitHub Actions environment variable name*'
304 { Set-CIEnv -Name '123start' -Value 'test' } | Should -Throw
305 }
306 }
307
308 Context 'In Azure DevOps environment' {
309 BeforeEach {
310 Clear-MockCIEnvironment
311 $env:TF_BUILD = 'True'
312 }
313
314 It 'Outputs task.setvariable format' {
315 $output = Set-CIEnv -Name 'test_var' -Value 'test-value'
316 $output | Should -Be '##vso[task.setvariable variable=test_var]test-value'
317 }
318
319 It 'Escapes semicolons in variable name to prevent property injection' {
320 $output = Set-CIEnv -Name 'test;isOutput=true' -Value 'value'
321 $output | Should -Match '%AZP3B'
322 }
323 }
324
325 Context 'In local environment' {
326 BeforeEach {
327 Clear-MockCIEnvironment
328 }
329
330 It 'Does not produce console output' {
331 $output = Set-CIEnv -Name 'test_var' -Value 'test-value'
332 $output | Should -BeNullOrEmpty
333 }
334 }
335
336 Context 'GitHub with missing GITHUB_ENV' {
337 BeforeEach {
338 Clear-MockCIEnvironment
339 $env:GITHUB_ACTIONS = 'true'
340 }
341
342 It 'Handles missing GITHUB_ENV gracefully' {
343 { Set-CIEnv -Name 'test_var' -Value 'test-value' } | Should -Not -Throw
344 }
345 }
346}
347
348Describe 'Write-CIStepSummary' -Tag 'Unit' {
349 BeforeAll {
350 Save-CIEnvironment
351 }
352
353 AfterAll {
354 Restore-CIEnvironment
355 }
356
357 Context 'In GitHub Actions environment with Content' {
358 BeforeEach {
359 $script:mockFiles = Initialize-MockCIEnvironment
360 }
361
362 AfterEach {
363 Remove-MockCIFiles -MockFiles $script:mockFiles
364 }
365
366 It 'Writes content to GITHUB_STEP_SUMMARY file' {
367 Write-CIStepSummary -Content '## Test Summary'
368 $content = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
369 $content | Should -Match '## Test Summary'
370 }
371 }
372
373 Context 'In GitHub Actions environment with Path' {
374 BeforeEach {
375 $script:mockFiles = Initialize-MockCIEnvironment
376 $script:tempSummaryFile = Join-Path ([System.IO.Path]::GetTempPath()) 'test-summary.md'
377 '## Summary from file' | Set-Content -Path $script:tempSummaryFile
378 }
379
380 AfterEach {
381 Remove-MockCIFiles -MockFiles $script:mockFiles
382 Remove-Item -Path $script:tempSummaryFile -Force -ErrorAction SilentlyContinue
383 }
384
385 It 'Reads content from file path' {
386 Write-CIStepSummary -Path $script:tempSummaryFile
387 $content = Get-Content -Path $env:GITHUB_STEP_SUMMARY -Raw
388 $content | Should -Match '## Summary from file'
389 }
390 }
391
392 Context 'In Azure DevOps environment' {
393 BeforeEach {
394 Clear-MockCIEnvironment
395 $env:TF_BUILD = 'True'
396 }
397
398 It 'Outputs section header and content' {
399 $output = Write-CIStepSummary -Content '## Test Summary'
400 $output[0] | Should -Be '##[section]Step Summary'
401 $output[1] | Should -Be '## Test Summary'
402 }
403 }
404
405 Context 'In local environment' {
406 BeforeEach {
407 Clear-MockCIEnvironment
408 }
409
410 It 'Does not produce console output' {
411 $output = Write-CIStepSummary -Content '## Test Summary'
412 $output | Should -BeNullOrEmpty
413 }
414 }
415}
416
417Describe 'Write-CIAnnotation' -Tag 'Unit' {
418 BeforeAll {
419 Save-CIEnvironment
420 }
421
422 AfterAll {
423 Restore-CIEnvironment
424 }
425
426 Context 'In GitHub Actions environment' {
427 BeforeEach {
428 $script:mockFiles = Initialize-MockCIEnvironment
429 }
430
431 AfterEach {
432 Remove-MockCIFiles -MockFiles $script:mockFiles
433 }
434
435 It 'Outputs warning annotation' {
436 $output = Write-CIAnnotation -Message 'Test warning' -Level Warning
437 $output | Should -Be '::warning::Test warning'
438 }
439
440 It 'Outputs error annotation' {
441 $output = Write-CIAnnotation -Message 'Test error' -Level Error
442 $output | Should -Be '::error::Test error'
443 }
444
445 It 'Outputs notice annotation' {
446 $output = Write-CIAnnotation -Message 'Test notice' -Level Notice
447 $output | Should -Be '::notice::Test notice'
448 }
449
450 It 'Includes file in annotation' {
451 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'src/test.ps1'
452 $output | Should -Be '::warning file=src/test.ps1::Test'
453 }
454
455 It 'Normalizes backslashes to forward slashes' {
456 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'src\path\test.ps1'
457 $output | Should -Be '::warning file=src/path/test.ps1::Test'
458 }
459
460 It 'Includes line number in annotation' {
461 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'test.ps1' -Line 42
462 $output | Should -Be '::warning file=test.ps1,line=42::Test'
463 }
464
465 It 'Includes column number in annotation' {
466 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'test.ps1' -Line 42 -Column 10
467 $output | Should -Be '::warning file=test.ps1,line=42,col=10::Test'
468 }
469
470 It 'Defaults to Warning level' {
471 $output = Write-CIAnnotation -Message 'Test message'
472 $output | Should -Be '::warning::Test message'
473 }
474 }
475
476 Context 'In Azure DevOps environment' {
477 BeforeEach {
478 Clear-MockCIEnvironment
479 $env:TF_BUILD = 'True'
480 }
481
482 It 'Outputs task.logissue for warning' {
483 $output = Write-CIAnnotation -Message 'Test warning' -Level Warning
484 $output | Should -Be '##vso[task.logissue type=warning]Test warning'
485 }
486
487 It 'Outputs task.logissue for error' {
488 $output = Write-CIAnnotation -Message 'Test error' -Level Error
489 $output | Should -Be '##vso[task.logissue type=error]Test error'
490 }
491
492 It 'Maps Notice to info type' {
493 $output = Write-CIAnnotation -Message 'Test notice' -Level Notice
494 $output | Should -Be '##vso[task.logissue type=info]Test notice'
495 }
496
497 It 'Includes sourcepath for file' {
498 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'src/test.ps1'
499 $output | Should -Be '##vso[task.logissue type=warning;sourcepath=src/test.ps1]Test'
500 }
501
502 It 'Includes line and column numbers' {
503 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'test.ps1' -Line 42 -Column 10
504 $output | Should -Be '##vso[task.logissue type=warning;sourcepath=test.ps1;linenumber=42;columnnumber=10]Test'
505 }
506 }
507
508 Context 'In local environment' {
509 BeforeEach {
510 Clear-MockCIEnvironment
511 }
512
513 It 'Uses Write-Warning for all levels' {
514 # Write-Warning outputs to warning stream, not standard output
515 $output = Write-CIAnnotation -Message 'Test message' -Level Warning 3>&1
516 $output | Should -Match 'WARNING.*Test message'
517 }
518
519 It 'Includes file location in local output' {
520 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File 'test.ps1' -Line 42 3>&1
521 $output | Should -Match '\[test\.ps1:42\]'
522 }
523 }
524
525 Context 'Workflow command injection prevention (GitHub Actions)' {
526 BeforeEach {
527 $script:mockFiles = Initialize-MockCIEnvironment
528 }
529
530 AfterEach {
531 Remove-MockCIFiles -MockFiles $script:mockFiles
532 }
533
534 It 'Escapes newlines in message to prevent command injection' {
535 $maliciousMessage = "Test`n::set-output name=pwned::true"
536 $output = Write-CIAnnotation -Message $maliciousMessage -Level Warning
537 $output | Should -Not -Match '::set-output'
538 $output | Should -Match '%0A'
539 }
540
541 It 'Escapes carriage returns in message' {
542 $maliciousMessage = "Test`r::error::Injected"
543 $output = Write-CIAnnotation -Message $maliciousMessage -Level Warning
544 $output | Should -Not -Match '::error::Injected'
545 $output | Should -Match '%0D'
546 }
547
548 It 'Escapes percent signs in message' {
549 $maliciousMessage = 'Test %0A injection attempt'
550 $output = Write-CIAnnotation -Message $maliciousMessage -Level Warning
551 $output | Should -Match '%250A'
552 }
553
554 It 'Escapes colons and commas in file path' {
555 $maliciousFile = 'file:injection,col=1'
556 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File $maliciousFile
557 $output | Should -Match '%3A'
558 $output | Should -Match '%2C'
559 }
560
561 It 'Prevents full command injection via file parameter' {
562 $maliciousFile = "path`n::error::Pwned"
563 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File $maliciousFile
564 $output | Should -Not -Match '::error::Pwned'
565 }
566 }
567
568 Context 'Workflow command injection prevention (Azure DevOps)' {
569 BeforeEach {
570 Clear-MockCIEnvironment
571 $env:TF_BUILD = 'True'
572 }
573
574 It 'Escapes newlines in message to prevent command injection' {
575 $maliciousMessage = "Test`n##vso[task.setvariable variable=pwned]true"
576 $output = Write-CIAnnotation -Message $maliciousMessage -Level Warning
577 $output | Should -Not -Match '##vso\[task\.setvariable'
578 $output | Should -Match '%AZP0A'
579 }
580
581 It 'Escapes closing brackets in file path' {
582 $maliciousFile = 'path]##vso[task.setvariable variable=pwned]true'
583 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File $maliciousFile
584 $output | Should -Match '%AZP5D'
585 }
586
587 It 'Escapes semicolons in file path' {
588 $maliciousFile = 'path;linenumber=999'
589 $output = Write-CIAnnotation -Message 'Test' -Level Warning -File $maliciousFile
590 $output | Should -Match '%AZP3B'
591 }
592
593 It 'Prevents full command injection via message' {
594 $maliciousMessage = "Test`n##vso[task.complete result=Failed]"
595 $output = Write-CIAnnotation -Message $maliciousMessage -Level Warning
596 $output | Should -Not -Match '##vso\[task\.complete'
597 }
598 }
599}
600
601Describe 'Write-CIAnnotations' -Tag 'Unit' {
602 BeforeAll {
603 Save-CIEnvironment
604 }
605
606 AfterAll {
607 Restore-CIEnvironment
608 }
609
610 Context 'In GitHub Actions environment' {
611 BeforeEach {
612 $script:mockFiles = Initialize-MockCIEnvironment
613 }
614
615 AfterEach {
616 Remove-MockCIFiles -MockFiles $script:mockFiles
617 }
618
619 It 'Outputs error and warning annotations from summary' {
620 $summary = [pscustomobject]@{
621 Results = @(
622 [pscustomobject]@{
623 RelativePath = 'test.md'
624 Issues = @(
625 [pscustomobject]@{ Type = 'Error'; Message = 'Test error'; Line = 42 },
626 [pscustomobject]@{ Type = 'Warning'; Message = 'Test warning'; Line = 0 }
627 )
628 }
629 )
630 }
631
632 $output = Write-CIAnnotations -Summary $summary
633 $output | Should -Contain '::error file=test.md,line=42::Test error'
634 $output | Should -Contain '::warning file=test.md,line=1::Test warning'
635 }
636
637 It 'Escapes newlines in message' {
638 $summary = [pscustomobject]@{
639 Results = @(
640 [pscustomobject]@{
641 RelativePath = 'test.md'
642 Issues = @(
643 [pscustomobject]@{ Type = 'Error'; Message = "line1`nline2"; Line = 1 }
644 )
645 }
646 )
647 }
648
649 $output = Write-CIAnnotations -Summary $summary
650 $output | Should -Match 'line1%0Aline2'
651 }
652 }
653
654 Context 'In Azure DevOps environment' {
655 BeforeEach {
656 Clear-MockCIEnvironment
657 $env:TF_BUILD = 'True'
658 }
659
660 It 'Outputs task.logissue entries for issues' {
661 $summary = [pscustomobject]@{
662 Results = @(
663 [pscustomobject]@{
664 RelativePath = 'test.md'
665 Issues = @(
666 [pscustomobject]@{ Type = 'Error'; Message = 'Test error'; Line = 10; Column = 4 }
667 )
668 }
669 )
670 }
671
672 $output = Write-CIAnnotations -Summary $summary
673 $output | Should -Be '##vso[task.logissue type=error;sourcepath=test.md;linenumber=10;columnnumber=4]Test error'
674 }
675 }
676
677 Context 'In local environment' {
678 BeforeEach {
679 Clear-MockCIEnvironment
680 }
681
682 It 'Does not throw when emitting annotations' {
683 $summary = [pscustomobject]@{
684 Results = @(
685 [pscustomobject]@{
686 RelativePath = 'test.md'
687 Issues = @(
688 [pscustomobject]@{ Type = 'Warning'; Message = 'Test warning'; Line = 2 }
689 )
690 }
691 )
692 }
693
694 { Write-CIAnnotations -Summary $summary } | Should -Not -Throw
695 }
696 }
697
698 Context 'With no issues' {
699 BeforeEach {
700 Clear-MockCIEnvironment
701 }
702
703 It 'Returns nothing when no issues exist' {
704 $summary = [pscustomobject]@{
705 Results = @(
706 [pscustomobject]@{ RelativePath = 'test.md'; Issues = @() }
707 )
708 }
709
710 $output = Write-CIAnnotations -Summary $summary
711 $output | Should -BeNullOrEmpty
712 }
713 }
714}
715
716Describe 'Set-CITaskResult' -Tag 'Unit' {
717 BeforeAll {
718 Save-CIEnvironment
719 }
720
721 AfterAll {
722 Restore-CIEnvironment
723 }
724
725 Context 'In GitHub Actions environment' {
726 BeforeEach {
727 $script:mockFiles = Initialize-MockCIEnvironment
728 }
729
730 AfterEach {
731 Remove-MockCIFiles -MockFiles $script:mockFiles
732 }
733
734 It 'Outputs error for Failed result' {
735 $output = Set-CITaskResult -Result Failed
736 $output | Should -Be '::error::Task failed'
737 }
738
739 It 'Does not output for Succeeded result' {
740 $output = Set-CITaskResult -Result Succeeded
741 $output | Should -BeNullOrEmpty
742 }
743
744 It 'Does not output for SucceededWithIssues result' {
745 $output = Set-CITaskResult -Result SucceededWithIssues
746 $output | Should -BeNullOrEmpty
747 }
748 }
749
750 Context 'In Azure DevOps environment' {
751 BeforeEach {
752 Clear-MockCIEnvironment
753 $env:TF_BUILD = 'True'
754 }
755
756 It 'Outputs task.complete for Succeeded' {
757 $output = Set-CITaskResult -Result Succeeded
758 $output | Should -Be '##vso[task.complete result=Succeeded]'
759 }
760
761 It 'Outputs task.complete for SucceededWithIssues' {
762 $output = Set-CITaskResult -Result SucceededWithIssues
763 $output | Should -Be '##vso[task.complete result=SucceededWithIssues]'
764 }
765
766 It 'Outputs task.complete for Failed' {
767 $output = Set-CITaskResult -Result Failed
768 $output | Should -Be '##vso[task.complete result=Failed]'
769 }
770 }
771
772 Context 'In local environment' {
773 BeforeEach {
774 Clear-MockCIEnvironment
775 }
776
777 It 'Does not produce console output' {
778 $output = Set-CITaskResult -Result Succeeded
779 $output | Should -BeNullOrEmpty
780 }
781 }
782}
783
784Describe 'Publish-CIArtifact' -Tag 'Unit' {
785 BeforeAll {
786 Save-CIEnvironment
787 }
788
789 AfterAll {
790 Restore-CIEnvironment
791 }
792
793 Context 'In GitHub Actions environment' {
794 BeforeEach {
795 $script:mockFiles = Initialize-MockCIEnvironment
796 $script:tempArtifact = Join-Path ([System.IO.Path]::GetTempPath()) 'test-artifact.txt'
797 'artifact content' | Set-Content -Path $script:tempArtifact
798 }
799
800 AfterEach {
801 Remove-MockCIFiles -MockFiles $script:mockFiles
802 Remove-Item -Path $script:tempArtifact -Force -ErrorAction SilentlyContinue
803 }
804
805 It 'Sets artifact outputs' {
806 Publish-CIArtifact -Path $script:tempArtifact -Name 'test-artifact'
807 $content = Get-Content -Path $env:GITHUB_OUTPUT -Raw
808 $content | Should -Match "artifact-path-test-artifact=$([regex]::Escape($script:tempArtifact))"
809 $content | Should -Match 'artifact-name-test-artifact=test-artifact'
810 }
811 }
812
813 Context 'In Azure DevOps environment' {
814 BeforeEach {
815 Clear-MockCIEnvironment
816 $env:TF_BUILD = 'True'
817 $script:tempArtifact = Join-Path ([System.IO.Path]::GetTempPath()) 'test-artifact.txt'
818 'artifact content' | Set-Content -Path $script:tempArtifact
819 }
820
821 AfterEach {
822 Remove-Item -Path $script:tempArtifact -Force -ErrorAction SilentlyContinue
823 }
824
825 It 'Outputs artifact.upload command' {
826 $output = Publish-CIArtifact -Path $script:tempArtifact -Name 'test-artifact'
827 $output | Should -Match '##vso\[artifact\.upload containerfolder=test-artifact;artifactname=test-artifact\]'
828 }
829
830 It 'Uses ContainerFolder when specified' {
831 $output = Publish-CIArtifact -Path $script:tempArtifact -Name 'test-artifact' -ContainerFolder 'custom-folder'
832 $output | Should -Match '##vso\[artifact\.upload containerfolder=custom-folder;artifactname=test-artifact\]'
833 }
834 }
835
836 Context 'With non-existent path' {
837 BeforeEach {
838 Clear-MockCIEnvironment
839 $env:TF_BUILD = 'True'
840 }
841
842 It 'Outputs warning for missing path' {
843 $warning = $null
844 Publish-CIArtifact -Path 'C:\nonexistent\file.txt' -Name 'test' -WarningVariable warning 3>&1
845 $warning | Should -Match 'Artifact path not found'
846 }
847
848 It 'Does not produce command output for missing path' {
849 $output = Publish-CIArtifact -Path 'C:\nonexistent\file.txt' -Name 'test' 3>$null
850 $output | Should -BeNullOrEmpty
851 }
852 }
853
854 Context 'In local environment' {
855 BeforeEach {
856 Clear-MockCIEnvironment
857 $script:tempArtifact = Join-Path ([System.IO.Path]::GetTempPath()) 'test-artifact.txt'
858 'artifact content' | Set-Content -Path $script:tempArtifact
859 }
860
861 AfterEach {
862 Remove-Item -Path $script:tempArtifact -Force -ErrorAction SilentlyContinue
863 }
864
865 It 'Does not produce console output' {
866 $output = Publish-CIArtifact -Path $script:tempArtifact -Name 'test-artifact'
867 $output | Should -BeNullOrEmpty
868 }
869 }
870}
871