Skip to content

Commit 2fcd595

Browse files
test(scripts): fix Pester assertion syntax and add tar extraction tests (#302)
## Description Fixes Pester assertion syntax error and expands test coverage for `Get-VerifiedDownload.ps1` with tar archive extraction paths. - Fixed `Should -Invoke` syntax error (`-Exactly 1` → `-Times 1 -Exactly`) - Added 6 tar extraction test cases covering tar.gz success, plain tar success, tar unavailable errors, and extraction failure handling - Added test for plain `.tar` archive type detection - Raised code coverage target from 70% to 80% - Removed unused `$Uri` parameter from mock blocks to satisfy PSScriptAnalyzer ## Related Issue(s) Resolves #264 ## Type of Change Select all that apply: **Code & Documentation:** - [x] Bug fix (non-breaking change fixing an issue) - [ ] New feature (non-breaking change adding functionality) - [ ] Breaking change (fix or feature causing existing functionality to change) - [ ] Documentation update **Infrastructure & Configuration:** - [ ] GitHub Actions workflow - [ ] Linting configuration (markdown, PowerShell, etc.) - [ ] Security configuration - [ ] DevContainer configuration - [ ] Dependency update **AI Artifacts:** - [ ] Reviewed contribution with `prompt-builder` agent and addressed all feedback - [ ] Copilot instructions (`.github/instructions/*.instructions.md`) - [ ] Copilot prompt (`.github/prompts/*.prompt.md`) - [ ] Copilot agent (`.github/agents/*.agent.md`) > **Note for AI Artifact Contributors**: > > - **Agents**: Research, indexing/referencing other project (using standard VS Code GitHub Copilot/MCP tools), planning, and general implementation agents likely already exist. Review `.github/agents/` before creating new ones. > - **Model Versions**: Only contributions targeting the **latest Anthropic and OpenAI models** will be accepted. Older model versions (e.g., GPT-3.5, Claude 3) will be rejected. > - See [Agents Not Accepted](../docs/contributing/custom-agents.md#agents-not-accepted) and [Model Version Requirements](../docs/contributing/ai-artifacts-common.md#model-version-requirements). **Other:** - [x] Script/automation (`.ps1`, `.sh`, `.py`) - [ ] Other (please describe): ## Testing - Ran `Invoke-Pester` to verify all 37 tests pass - Confirmed code coverage increased to 82.76% (exceeds 80% target) - Ran `Invoke-ScriptAnalyzer` with project settings to verify 0 warnings ## Checklist ### Required Checks - [ ] Documentation is updated (if applicable) - [x] Files follow existing naming conventions - [x] Changes are backwards compatible (if applicable) - [x] Tests added for new functionality (if applicable) ### AI Artifact Contributions N/A - Not an AI artifact contribution. ### Required Automated Checks The following validation commands must pass before merging: - [ ] Markdown linting: `npm run lint:md` - [ ] Spell checking: `npm run spell-check` - [ ] Frontmatter validation: `npm run lint:frontmatter` - [ ] Link validation: `npm run lint:md-links` - [ ] PowerShell analysis: `npm run lint:ps` ## Security Considerations - [x] This PR does not contain any sensitive or NDA information - [ ] Any new dependencies have been reviewed for security issues - [x] Security-related scripts follow the principle of least privilege ## Additional Notes Coverage improvement addresses issue #264 by testing previously uncovered tar extraction branches in `Get-VerifiedDownload.ps1`. 🧪 - Generated by Copilot
1 parent aeaed13 commit 2fcd595

File tree

2 files changed

+371
-1
lines changed

2 files changed

+371
-1
lines changed

scripts/tests/lib/Get-VerifiedDownload.Tests.ps1

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ Describe 'Get-ArchiveType' {
139139
$result | Should -Be 'tar.gz'
140140
}
141141

142+
It 'Returns tar for plain .tar files' {
143+
$result = Get-ArchiveType -Path 'archive.tar'
144+
$result | Should -Be 'tar'
145+
}
146+
142147
It 'Returns zip for .zip files' {
143148
$result = Get-ArchiveType -Path 'archive.zip'
144149
$result | Should -Be 'zip'
@@ -161,3 +166,368 @@ Describe 'Test-TarAvailable' {
161166
$result | Should -BeOfType [bool]
162167
}
163168
}
169+
170+
Describe 'Invoke-VerifiedDownload' {
171+
BeforeAll {
172+
$script:testDestDir = Join-Path $TestDrive 'downloads'
173+
$script:testUrl = 'https://example.com/tool.zip'
174+
$script:testHash = 'ABC123DEF456ABC123DEF456ABC123DEF456ABC123DEF456ABC123DEF456ABCD'
175+
}
176+
177+
BeforeEach {
178+
# Clean test directory before each test
179+
if (Test-Path $script:testDestDir) {
180+
Remove-Item -Path $script:testDestDir -Recurse -Force
181+
}
182+
}
183+
184+
Context 'Skip when valid file exists' {
185+
BeforeAll {
186+
Mock Write-Verbose { }
187+
}
188+
189+
It 'Skips download when existing file hash matches' {
190+
# Create a pre-existing file with matching hash
191+
New-Item -ItemType Directory -Path $script:testDestDir -Force | Out-Null
192+
$prePath = Join-Path $script:testDestDir 'tool.zip'
193+
'existing content' | Set-Content -Path $prePath -NoNewline
194+
$actualHash = (Get-FileHash -Path $prePath -Algorithm SHA256).Hash
195+
196+
$result = Invoke-VerifiedDownload `
197+
-Url $script:testUrl `
198+
-DestinationDirectory $script:testDestDir `
199+
-ExpectedHash $actualHash
200+
201+
$result.WasDownloaded | Should -BeFalse
202+
$result.HashVerified | Should -BeTrue
203+
$result.Path | Should -Be $prePath
204+
}
205+
206+
It 'Returns existing file path in result' {
207+
New-Item -ItemType Directory -Path $script:testDestDir -Force | Out-Null
208+
$prePath = Join-Path $script:testDestDir 'tool.zip'
209+
'existing content' | Set-Content -Path $prePath -NoNewline
210+
$actualHash = (Get-FileHash -Path $prePath -Algorithm SHA256).Hash
211+
212+
$result = Invoke-VerifiedDownload `
213+
-Url $script:testUrl `
214+
-DestinationDirectory $script:testDestDir `
215+
-ExpectedHash $actualHash
216+
217+
$result.Path | Should -Not -BeNullOrEmpty
218+
$result.Path | Should -Exist
219+
}
220+
}
221+
222+
Context 'Successful download with valid hash' {
223+
BeforeAll {
224+
Mock Write-Host { }
225+
}
226+
227+
It 'Downloads file and returns success result' {
228+
$expectedHash = '2D8C2F6D7A37F9F6E8C5A4B3D2E1F0A9B8C7D6E5F4A3B2C1D0E9F8A7B6C5D4E3'
229+
230+
Mock Invoke-WebRequest {
231+
param($OutFile)
232+
Set-Content -Path $OutFile -Value 'mock downloaded content' -NoNewline
233+
}
234+
Mock Get-FileHashValue { return '2D8C2F6D7A37F9F6E8C5A4B3D2E1F0A9B8C7D6E5F4A3B2C1D0E9F8A7B6C5D4E3' }
235+
236+
$result = Invoke-VerifiedDownload `
237+
-Url $script:testUrl `
238+
-DestinationDirectory $script:testDestDir `
239+
-ExpectedHash $expectedHash
240+
241+
$result.WasDownloaded | Should -BeTrue
242+
$result.HashVerified | Should -BeTrue
243+
$result.Path | Should -Exist
244+
Should -Invoke -CommandName Invoke-WebRequest -Times 1 -Exactly
245+
Should -Invoke -CommandName Get-FileHashValue -Times 1
246+
}
247+
248+
It 'Creates destination directory if not exists' {
249+
$expectedHash = 'AABBCC11223344556677889900AABBCC11223344556677889900AABBCC112233'
250+
251+
Mock Invoke-WebRequest {
252+
param($OutFile)
253+
Set-Content -Path $OutFile -Value 'mock content' -NoNewline
254+
}
255+
Mock Get-FileHashValue { return 'AABBCC11223344556677889900AABBCC11223344556677889900AABBCC112233' }
256+
257+
$newDir = Join-Path $TestDrive 'new-subdir'
258+
$result = Invoke-VerifiedDownload `
259+
-Url $script:testUrl `
260+
-DestinationDirectory $newDir `
261+
-ExpectedHash $expectedHash
262+
263+
$newDir | Should -Exist
264+
$result.WasDownloaded | Should -BeTrue
265+
}
266+
}
267+
268+
Context 'Successful download with extraction' {
269+
BeforeAll {
270+
Mock Write-Host { }
271+
Mock Write-Verbose { }
272+
}
273+
274+
It 'Extracts ZIP archive to target path' {
275+
$downloadedContent = 'mock zip content'
276+
$expectedHash = 'ZIPARCHIVEHASH123456789012345678901234567890123456789012345678'
277+
278+
Mock Invoke-WebRequest {
279+
param($OutFile)
280+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
281+
}
282+
Mock Get-FileHashValue { return $expectedHash }
283+
Mock Expand-Archive { }
284+
Mock Get-ArchiveType { return 'zip' }
285+
286+
$extractDir = Join-Path $TestDrive 'extracted'
287+
$result = Invoke-VerifiedDownload `
288+
-Url 'https://example.com/archive.zip' `
289+
-DestinationDirectory $script:testDestDir `
290+
-ExpectedHash $expectedHash `
291+
-Extract `
292+
-ExtractPath $extractDir
293+
294+
$result.HashVerified | Should -BeTrue
295+
Should -Invoke -CommandName Expand-Archive -Times 1 -Exactly
296+
}
297+
}
298+
299+
Context 'Hash mismatch' {
300+
BeforeAll {
301+
Mock Write-Host { }
302+
}
303+
304+
It 'Throws on hash verification failure' {
305+
Mock Invoke-WebRequest {
306+
param($OutFile)
307+
Set-Content -Path $OutFile -Value 'bad content' -NoNewline
308+
}
309+
Mock Get-FileHashValue { return 'WRONGHASH123456789012345678901234567890123456789012345678901234' }
310+
311+
{ Invoke-VerifiedDownload `
312+
-Url $script:testUrl `
313+
-DestinationDirectory $script:testDestDir `
314+
-ExpectedHash 'EXPECTEDHASH567890123456789012345678901234567890123456789012345'
315+
} | Should -Throw '*Checksum verification failed*'
316+
}
317+
318+
It 'Cleans up temp file on hash failure' {
319+
Mock Invoke-WebRequest {
320+
param($OutFile)
321+
Set-Content -Path $OutFile -Value 'bad content' -NoNewline
322+
}
323+
Mock Get-FileHashValue { return 'MISMATCHHASH23456789012345678901234567890123456789012345678901' }
324+
Mock Remove-Item { } -Verifiable
325+
326+
{ Invoke-VerifiedDownload `
327+
-Url $script:testUrl `
328+
-DestinationDirectory $script:testDestDir `
329+
-ExpectedHash 'EXPECTEDHASH567890123456789012345678901234567890123456789012345'
330+
} | Should -Throw
331+
332+
# The finally block should clean up temp files
333+
Should -InvokeVerifiable
334+
}
335+
}
336+
337+
Context 'Network error' {
338+
It 'Propagates network errors' {
339+
Mock Invoke-WebRequest { throw 'Network unreachable' }
340+
341+
{ Invoke-VerifiedDownload `
342+
-Url $script:testUrl `
343+
-DestinationDirectory $script:testDestDir `
344+
-ExpectedHash $script:testHash
345+
} | Should -Throw '*Network unreachable*'
346+
}
347+
}
348+
349+
Context 'Extraction error' {
350+
BeforeAll {
351+
Mock Write-Host { }
352+
Mock Write-Verbose { }
353+
}
354+
355+
It 'Propagates ZIP extraction errors' {
356+
$downloadedContent = 'mock content'
357+
$expectedHash = 'VALIDHASH1234567890123456789012345678901234567890123456789012'
358+
359+
Mock Invoke-WebRequest {
360+
param($OutFile)
361+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
362+
}
363+
Mock Get-FileHashValue { return $expectedHash }
364+
Mock Expand-Archive { throw 'Invalid archive format' }
365+
Mock Get-ArchiveType { return 'zip' }
366+
367+
{ Invoke-VerifiedDownload `
368+
-Url 'https://example.com/archive.zip' `
369+
-DestinationDirectory $script:testDestDir `
370+
-ExpectedHash $expectedHash `
371+
-Extract
372+
} | Should -Throw '*Invalid archive format*'
373+
}
374+
375+
It 'Throws for unsupported archive format' {
376+
$downloadedContent = 'mock content'
377+
$expectedHash = 'UNSUPPORTEDHASH89012345678901234567890123456789012345678901234'
378+
379+
Mock Invoke-WebRequest {
380+
param($OutFile)
381+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
382+
}
383+
Mock Get-FileHashValue { return $expectedHash }
384+
Mock Get-ArchiveType { return 'unknown' }
385+
386+
{ Invoke-VerifiedDownload `
387+
-Url 'https://example.com/file.xyz' `
388+
-DestinationDirectory $script:testDestDir `
389+
-ExpectedHash $expectedHash `
390+
-Extract
391+
} | Should -Throw '*Unsupported archive format*'
392+
}
393+
}
394+
395+
Context 'Tar extraction' {
396+
BeforeAll {
397+
Mock Write-Host { }
398+
Mock Write-Verbose { }
399+
}
400+
401+
It 'Throws when tar not available for tar.gz extraction' {
402+
$downloadedContent = 'mock content'
403+
$expectedHash = 'TARHASH123456789012345678901234567890123456789012345678901234'
404+
405+
Mock Invoke-WebRequest {
406+
param($OutFile)
407+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
408+
}
409+
Mock Get-FileHashValue { return $expectedHash }
410+
Mock Get-ArchiveType { return 'tar.gz' }
411+
Mock Test-TarAvailable { return $false }
412+
413+
{ Invoke-VerifiedDownload `
414+
-Url 'https://example.com/archive.tar.gz' `
415+
-DestinationDirectory $script:testDestDir `
416+
-ExpectedHash $expectedHash `
417+
-Extract
418+
} | Should -Throw '*tar command not available*'
419+
}
420+
421+
It 'Extracts tar.gz archive when tar is available' {
422+
$downloadedContent = 'mock tar.gz content'
423+
$expectedHash = 'TARGZHASH23456789012345678901234567890123456789012345678901234'
424+
425+
Mock Invoke-WebRequest {
426+
param($OutFile)
427+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
428+
}
429+
Mock Get-FileHashValue { return $expectedHash }
430+
Mock Get-ArchiveType { return 'tar.gz' }
431+
Mock Test-TarAvailable { return $true }
432+
Mock tar { $global:LASTEXITCODE = 0 }
433+
434+
$extractDir = Join-Path $TestDrive 'tar-gz-extracted'
435+
$result = Invoke-VerifiedDownload `
436+
-Url 'https://example.com/archive.tar.gz' `
437+
-DestinationDirectory $script:testDestDir `
438+
-ExpectedHash $expectedHash `
439+
-Extract `
440+
-ExtractPath $extractDir
441+
442+
$result.HashVerified | Should -BeTrue
443+
Should -Invoke -CommandName tar -Times 1 -Exactly
444+
}
445+
446+
It 'Extracts plain tar archive when tar is available' {
447+
$downloadedContent = 'mock tar content'
448+
$expectedHash = 'PLAINTARHASH456789012345678901234567890123456789012345678901234'
449+
450+
Mock Invoke-WebRequest {
451+
param($OutFile)
452+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
453+
}
454+
Mock Get-FileHashValue { return $expectedHash }
455+
Mock Get-ArchiveType { return 'tar' }
456+
Mock Test-TarAvailable { return $true }
457+
Mock tar { $global:LASTEXITCODE = 0 }
458+
459+
$extractDir = Join-Path $TestDrive 'tar-extracted'
460+
$result = Invoke-VerifiedDownload `
461+
-Url 'https://example.com/archive.tar' `
462+
-DestinationDirectory $script:testDestDir `
463+
-ExpectedHash $expectedHash `
464+
-Extract `
465+
-ExtractPath $extractDir
466+
467+
$result.HashVerified | Should -BeTrue
468+
Should -Invoke -CommandName tar -Times 1 -Exactly
469+
}
470+
471+
It 'Throws when tar not available for plain tar extraction' {
472+
$downloadedContent = 'mock content'
473+
$expectedHash = 'NOTARHASH789012345678901234567890123456789012345678901234567890'
474+
475+
Mock Invoke-WebRequest {
476+
param($OutFile)
477+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
478+
}
479+
Mock Get-FileHashValue { return $expectedHash }
480+
Mock Get-ArchiveType { return 'tar' }
481+
Mock Test-TarAvailable { return $false }
482+
483+
{ Invoke-VerifiedDownload `
484+
-Url 'https://example.com/archive.tar' `
485+
-DestinationDirectory $script:testDestDir `
486+
-ExpectedHash $expectedHash `
487+
-Extract
488+
} | Should -Throw '*tar command not available*'
489+
}
490+
491+
It 'Throws when tar.gz extraction fails with non-zero exit code' {
492+
$downloadedContent = 'mock content'
493+
$expectedHash = 'TARFAILHASH9012345678901234567890123456789012345678901234567890'
494+
495+
Mock Invoke-WebRequest {
496+
param($OutFile)
497+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
498+
}
499+
Mock Get-FileHashValue { return $expectedHash }
500+
Mock Get-ArchiveType { return 'tar.gz' }
501+
Mock Test-TarAvailable { return $true }
502+
Mock tar { $global:LASTEXITCODE = 1 }
503+
504+
{ Invoke-VerifiedDownload `
505+
-Url 'https://example.com/archive.tar.gz' `
506+
-DestinationDirectory $script:testDestDir `
507+
-ExpectedHash $expectedHash `
508+
-Extract
509+
} | Should -Throw '*tar extraction failed*'
510+
}
511+
512+
It 'Throws when plain tar extraction fails with non-zero exit code' {
513+
$downloadedContent = 'mock content'
514+
$expectedHash = 'PLAINTARFAIL012345678901234567890123456789012345678901234567890'
515+
516+
Mock Invoke-WebRequest {
517+
param($OutFile)
518+
Set-Content -Path $OutFile -Value $downloadedContent -NoNewline
519+
}
520+
Mock Get-FileHashValue { return $expectedHash }
521+
Mock Get-ArchiveType { return 'tar' }
522+
Mock Test-TarAvailable { return $true }
523+
Mock tar { $global:LASTEXITCODE = 2 }
524+
525+
{ Invoke-VerifiedDownload `
526+
-Url 'https://example.com/archive.tar' `
527+
-DestinationDirectory $script:testDestDir `
528+
-ExpectedHash $expectedHash `
529+
-Extract
530+
} | Should -Throw '*tar extraction failed*'
531+
}
532+
}
533+
}

scripts/tests/pester.config.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ if ($CodeCoverage.IsPresent) {
6161
}
6262

6363
$configuration.CodeCoverage.ExcludeTests = $true
64-
$configuration.CodeCoverage.CoveragePercentTarget = 70
64+
$configuration.CodeCoverage.CoveragePercentTarget = 80
6565
}
6666

6767
# Should configuration

0 commit comments

Comments
 (0)