microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/a11y-pr1-scripts-validators

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/tests/linting/Invoke-PSScriptAnalyzer.Tests.ps1

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