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/linting/Invoke-PSScriptAnalyzer.Tests.ps1

384lines · modecode

1#Requires -Modules Pester
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Pester tests for Invoke-PSScriptAnalyzer.ps1 script
7.DESCRIPTION
8 Tests for PSScriptAnalyzer wrapper script:
9 - Parameter validation
10 - Module availability checks
11 - ChangedFilesOnly filtering
12 - CI integration
13#>
14
15BeforeAll {
16 $script:ScriptPath = Join-Path $PSScriptRoot '../../linting/Invoke-PSScriptAnalyzer.ps1'
17 $script:ModulePath = Join-Path $PSScriptRoot '../../linting/Modules/LintingHelpers.psm1'
18 $script:CIHelpersPath = Join-Path $PSScriptRoot '../../lib/Modules/CIHelpers.psm1'
19
20 # Import modules for mocking
21 Import-Module $script:ModulePath -Force
22 Import-Module $script:CIHelpersPath -Force
23
24 . $script:ScriptPath
25}
26
27AfterAll {
28 Remove-Module LintingHelpers -Force -ErrorAction SilentlyContinue
29 Remove-Module CIHelpers -Force -ErrorAction SilentlyContinue
30}
31
32#region Parameter Validation Tests
33
34Describe 'Invoke-PSScriptAnalyzer Parameter Validation' -Tag 'Unit' {
35 Context 'ChangedFilesOnly parameter' {
36 BeforeEach {
37 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
38 Mock Invoke-ScriptAnalyzer { @() }
39 Mock Get-ChangedFilesFromGit { @('script.ps1') }
40 Mock Get-FilesRecursive { @() }
41 Mock Set-CIOutput {}
42 Mock Set-CIEnv {}
43 Mock Write-CIStepSummary {}
44 Mock Write-CIAnnotation {}
45 Mock Out-File {}
46 }
47
48 It 'Accepts ChangedFilesOnly switch' {
49 { Invoke-PSScriptAnalyzerCore -ChangedFilesOnly } | Should -Not -Throw
50 }
51
52 It 'Accepts BaseBranch with ChangedFilesOnly' {
53 { Invoke-PSScriptAnalyzerCore -ChangedFilesOnly -BaseBranch 'develop' } | Should -Not -Throw
54 }
55 }
56
57 Context 'ConfigPath parameter' {
58 BeforeEach {
59 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
60 Mock Invoke-ScriptAnalyzer { @() }
61 Mock Get-FilesRecursive { @() }
62 Mock Set-CIOutput {}
63 Mock Set-CIEnv {}
64 Mock Write-CIStepSummary {}
65 Mock Write-CIAnnotation {}
66 Mock Out-File {}
67 }
68
69 It 'Uses default config path when not specified' {
70 # Script defaults to scripts/linting/PSScriptAnalyzer.psd1
71 { Invoke-PSScriptAnalyzerCore } | Should -Not -Throw
72 }
73
74 It 'Accepts custom config path' {
75 $configPath = Join-Path $PSScriptRoot '../../linting/PSScriptAnalyzer.psd1'
76 { Invoke-PSScriptAnalyzerCore -ConfigPath $configPath } | Should -Not -Throw
77 }
78 }
79
80 Context 'OutputPath parameter' {
81 BeforeEach {
82 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
83 Mock Invoke-ScriptAnalyzer { @() }
84 Mock Get-FilesRecursive { @() }
85 Mock Set-CIOutput {}
86 Mock Set-CIEnv {}
87 Mock Write-CIStepSummary {}
88 Mock Write-CIAnnotation {}
89 Mock Out-File {}
90 }
91
92 It 'Accepts custom output path' {
93 $outputPath = Join-Path ([System.IO.Path]::GetTempPath()) 'test-output.json'
94 { Invoke-PSScriptAnalyzerCore -OutputPath $outputPath } | Should -Not -Throw
95 }
96 }
97}
98
99#endregion
100
101#region Module Availability Tests
102
103Describe 'PSScriptAnalyzer Module Availability' -Tag 'Unit' {
104 Context 'Module not installed' {
105 BeforeEach {
106 Mock Get-Module { $null } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
107 Mock Install-Module {} -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
108 Mock Import-Module { throw 'Module not found' } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
109 Mock Write-Error {}
110 Mock Out-File {}
111 }
112
113 It 'Reports error when module unavailable' {
114 { Invoke-PSScriptAnalyzerCore } | Should -Throw
115 }
116 }
117
118 Context 'Module installed' {
119 BeforeEach {
120 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
121 Mock Invoke-ScriptAnalyzer { @() }
122 Mock Get-FilesRecursive { @() }
123 Mock Set-CIOutput {}
124 Mock Set-CIEnv {}
125 Mock Write-CIStepSummary {}
126 Mock Write-CIAnnotation {}
127 Mock Out-File {}
128 }
129
130 It 'Proceeds when module available' {
131 { Invoke-PSScriptAnalyzerCore } | Should -Not -Throw
132 }
133 }
134}
135
136#endregion
137
138#region File Discovery Tests
139
140Describe 'File Discovery' -Tag 'Unit' {
141 Context 'All files mode' {
142 BeforeEach {
143 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
144 Mock Invoke-ScriptAnalyzer { @() }
145 Mock Set-CIOutput {}
146 Mock Set-CIEnv {}
147 Mock Write-CIStepSummary {}
148 Mock Write-CIAnnotation {}
149 Mock Out-File {}
150 }
151
152 It 'Uses Get-FilesRecursive for all files' {
153 Mock Get-FilesRecursive {
154 return @('script1.ps1', 'script2.ps1')
155 }
156
157 Invoke-PSScriptAnalyzerCore
158 Should -Invoke Get-FilesRecursive -Times 1
159 }
160 }
161
162 Context 'Changed files only mode' {
163 BeforeEach {
164 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
165 Mock Invoke-ScriptAnalyzer { @() }
166 Mock Get-FilesRecursive { @() }
167 Mock Set-CIOutput {}
168 Mock Set-CIEnv {}
169 Mock Write-CIStepSummary {}
170 Mock Write-CIAnnotation {}
171 Mock Out-File {}
172 }
173
174 It 'Uses Get-ChangedFilesFromGit when ChangedFilesOnly specified' {
175 Mock Get-ChangedFilesFromGit {
176 return @('changed.ps1')
177 }
178
179 Invoke-PSScriptAnalyzerCore -ChangedFilesOnly
180 Should -Invoke Get-ChangedFilesFromGit -Times 1
181 }
182
183 It 'Passes BaseBranch to Get-ChangedFilesFromGit' {
184 Mock Get-ChangedFilesFromGit {
185 return @('changed.ps1')
186 }
187
188 Invoke-PSScriptAnalyzerCore -ChangedFilesOnly -BaseBranch 'develop'
189 Should -Invoke Get-ChangedFilesFromGit -Times 1 -ParameterFilter {
190 $BaseBranch -eq 'develop'
191 }
192 }
193 }
194}
195
196#endregion
197
198#region CI Integration Tests
199
200Describe 'CI Integration' -Tag 'Unit' {
201 Context 'Write-CIAnnotation calls' {
202 BeforeEach {
203 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
204 Mock Get-FilesRecursive { @('test.ps1') }
205 Mock Set-CIOutput {}
206 Mock Set-CIEnv {}
207 Mock Write-CIStepSummary {}
208 Mock Write-CIAnnotation {}
209 Mock Out-File {}
210 }
211
212 It 'Calls Write-CIAnnotation for each issue' {
213 Mock Invoke-ScriptAnalyzer {
214 return @(
215 [PSCustomObject]@{
216 ScriptPath = 'test.ps1'
217 Line = 10
218 Column = 5
219 RuleName = 'PSAvoidUsingInvokeExpression'
220 Severity = 'Warning'
221 Message = 'Avoid using Invoke-Expression'
222 }
223 )
224 }
225
226 try { Invoke-PSScriptAnalyzerCore } catch { $null = $_ }
227 Should -Invoke Write-CIAnnotation -Times 1
228 }
229
230 It 'Sets CI output for file count' {
231 Mock Invoke-ScriptAnalyzer { @() }
232
233 Invoke-PSScriptAnalyzerCore
234 Should -Invoke Set-CIOutput -Times 1 -ParameterFilter {
235 $Name -eq 'count'
236 }
237 }
238 }
239}
240
241#endregion
242
243#region Output Tests
244
245Describe 'Output Generation' -Tag 'Unit' {
246 BeforeAll {
247 $script:TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString())
248 New-Item -ItemType Directory -Path $script:TempDir -Force | Out-Null
249 }
250
251 AfterAll {
252 Remove-Item -Path $script:TempDir -Recurse -Force -ErrorAction SilentlyContinue
253 }
254
255 Context 'JSON output file' {
256 BeforeEach {
257 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
258 Mock Get-FilesRecursive { @('test.ps1') }
259 Mock Set-CIOutput {}
260 Mock Set-CIEnv {}
261 Mock Write-CIStepSummary {}
262 Mock Write-CIAnnotation {}
263
264 Mock Invoke-ScriptAnalyzer {
265 return @(
266 [PSCustomObject]@{
267 ScriptPath = 'test.ps1'
268 Line = 10
269 Column = 5
270 RuleName = 'TestRule'
271 Severity = 'Warning'
272 Message = 'Test message'
273 }
274 )
275 }
276
277 $script:OutputFile = Join-Path $script:TempDir 'output.json'
278 }
279
280 It 'Creates JSON output file' {
281 try { Invoke-PSScriptAnalyzerCore -OutputPath $script:OutputFile } catch { $null = $_ }
282 Test-Path $script:OutputFile | Should -BeTrue
283 }
284
285 It 'Output file contains valid JSON' {
286 try { Invoke-PSScriptAnalyzerCore -OutputPath $script:OutputFile } catch { $null = $_ }
287 { Get-Content $script:OutputFile | ConvertFrom-Json } | Should -Not -Throw
288 }
289 }
290}
291
292#endregion
293
294#region Exit Code Tests
295
296Describe 'Exit Code Handling' -Tag 'Unit' {
297 Context 'No issues found' {
298 BeforeEach {
299 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
300 Mock Get-FilesRecursive { @() }
301 Mock Set-CIOutput {}
302 Mock Set-CIEnv {}
303 Mock Write-CIStepSummary {}
304 Mock Write-CIAnnotation {}
305 Mock Invoke-ScriptAnalyzer { @() }
306 Mock Out-File {}
307 }
308
309 It 'Returns success when no issues' {
310 { Invoke-PSScriptAnalyzerCore } | Should -Not -Throw
311 }
312 }
313
314 Context 'Issues found' {
315 BeforeEach {
316 Mock Get-Module { $true } -ParameterFilter { $Name -eq 'PSScriptAnalyzer' }
317 Mock Get-FilesRecursive { @('test.ps1') }
318 Mock Set-CIOutput {}
319 Mock Set-CIEnv {}
320 Mock Write-CIStepSummary {}
321 Mock Write-CIAnnotation {}
322 Mock Out-File {}
323
324 Mock Invoke-ScriptAnalyzer {
325 return @(
326 [PSCustomObject]@{
327 ScriptPath = 'test.ps1'
328 Severity = 'Error'
329 RuleName = 'TestRule'
330 Message = 'Error found'
331 Line = 1
332 Column = 1
333 }
334 )
335 }
336 }
337
338 It 'Throws when issues found' {
339 { Invoke-PSScriptAnalyzerCore } | Should -Throw '*issue*'
340 }
341 }
342}
343
344#endregion
345
346#region PATH Sanitization Tests
347
348Describe 'PATH Sanitization Logic' -Tag 'Unit' {
349 # Validates the PATH filtering expression used in Main Execution to strip
350 # /mnt/* (WSL Windows mount) entries that cause slow 9P lookups.
351
352 It 'Strips /mnt/* entries from PATH' {
353 $sep = [System.IO.Path]::PathSeparator
354 $original = "/usr/bin${sep}/mnt/c/Windows/System32${sep}/home/user/bin${sep}/mnt/d/Tools"
355 $result = ($original -split [System.IO.Path]::PathSeparator |
356 Where-Object { $_ -notlike '/mnt/*' }) -join [System.IO.Path]::PathSeparator
357 $result | Should -Be "/usr/bin${sep}/home/user/bin"
358 }
359
360 It 'Preserves all entries when no /mnt/* paths present' {
361 $original = '/usr/bin:/home/user/bin:/usr/local/bin'
362 $result = ($original -split [System.IO.Path]::PathSeparator |
363 Where-Object { $_ -notlike '/mnt/*' }) -join [System.IO.Path]::PathSeparator
364 $result | Should -Be $original
365 }
366
367 It 'Handles PATH with only /mnt/* entries' {
368 $sep = [System.IO.Path]::PathSeparator
369 $original = "/mnt/c/Windows${sep}/mnt/d/Tools"
370 $result = ($original -split [System.IO.Path]::PathSeparator |
371 Where-Object { $_ -notlike '/mnt/*' }) -join [System.IO.Path]::PathSeparator
372 $result | Should -BeNullOrEmpty
373 }
374
375 It 'Does not strip similar but non-matching paths' {
376 $sep = [System.IO.Path]::PathSeparator
377 $original = "/mnt${sep}/usr/mnt/bin${sep}/home/mnt"
378 $result = ($original -split [System.IO.Path]::PathSeparator |
379 Where-Object { $_ -notlike '/mnt/*' }) -join [System.IO.Path]::PathSeparator
380 $result | Should -Be $original
381 }
382}
383
384#endregion
385