microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/collections-overview-docs

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/lib/CIHelpers.Tests.ps1

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