tests/Helpers.Tests.ps1
|
BeforeAll { $modulePath = Join-Path $PSScriptRoot ".." "WingetBatch.psd1" Import-Module $modulePath -Force -ErrorAction Stop } Describe "Compare-WingetVersion" { It "compares <A> vs <B> as <Expected>" -TestCases @( @{ A = '10.0'; B = '9.0'; Expected = 1 } @{ A = '2.9'; B = '2.10'; Expected = -1 } @{ A = '1.2.3'; B = '1.2.3.0'; Expected = 0 } @{ A = '1.0'; B = '1.0-beta'; Expected = 1 } @{ A = '1.0-beta2'; B = '1.0-beta10'; Expected = -1 } @{ A = 'v2.1'; B = '2.1'; Expected = 0 } @{ A = '2024.01.15'; B = '2023.12.31'; Expected = 1 } @{ A = '1.2.3.4.5'; B = '1.2.3.4.6'; Expected = -1 } @{ A = ''; B = '1.0'; Expected = -1 } ) { InModuleScope WingetBatch -Parameters @{ A = $A; B = $B; Expected = $Expected } { Compare-WingetVersion -ReferenceVersion $A -DifferenceVersion $B | Should -Be $Expected } } } Describe "Sort-WingetVersion" { It "sorts versions numerically, newest first" { InModuleScope WingetBatch { $sorted = Sort-WingetVersion -InputObject @('9.0', '10.0', '1.5', '10.0-rc1', '2.0') -Descending $sorted | Should -Be @('10.0', '10.0-rc1', '9.0', '2.0', '1.5') } } It "sorts objects by a property and returns a flat array" { InModuleScope WingetBatch { $objs = @([PSCustomObject]@{ Name = '1.9' }, [PSCustomObject]@{ Name = '1.10' }) $sorted = @(Sort-WingetVersion -InputObject $objs -Property Name -Descending) $sorted.Count | Should -Be 2 $sorted[0].Name | Should -Be '1.10' } } } Describe "ConvertTo-WingetActionResult" { It "treats Status Ok as success and carries RebootRequired" { InModuleScope WingetBatch { $r = ConvertTo-WingetActionResult -Result ([PSCustomObject]@{ Status = 'Ok'; RebootRequired = $true }) -Id 'A.B' -Action Install $r.Succeeded | Should -BeTrue $r.RebootRequired | Should -BeTrue } } It "reports an installer failure even though no exception was thrown" { InModuleScope WingetBatch { $r = ConvertTo-WingetActionResult -Result ([PSCustomObject]@{ Status = 'InstallError'; InstallerErrorCode = 1603 }) -Id 'A.B' -Action Install $r.Succeeded | Should -BeFalse $r.Message | Should -Match 'InstallError' $r.Message | Should -Match '1603' } } It "treats NoApplicableUpgrade as success for updates only" { InModuleScope WingetBatch { $res = [PSCustomObject]@{ Status = 'NoApplicableUpgrade' } (ConvertTo-WingetActionResult -Result $res -Id 'A.B' -Action Update).Succeeded | Should -BeTrue (ConvertTo-WingetActionResult -Result $res -Id 'A.B' -Action Install).Succeeded | Should -BeFalse } } It "fails when WinGet returns nothing" { InModuleScope WingetBatch { (ConvertTo-WingetActionResult -Result $null -Id 'A.B' -Action Uninstall).Succeeded | Should -BeFalse } } } Describe "Invoke-WingetPackageAction" { BeforeAll { # Stand-in for Microsoft.WinGet.Client\Install-WinGetPackage with the same core parameters function global:Test-FakeInstall { [CmdletBinding()] param($Id, $MatchOption, $Version, $Source, $Mode, [switch]$Force, $Scope) $global:FakeInstallArgs = $PSBoundParameters [PSCustomObject]@{ Status = $global:FakeInstallStatus; RebootRequired = $false } } } AfterAll { Remove-Item function:global:Test-FakeInstall -ErrorAction SilentlyContinue } It "matches the ID exactly and passes only supported, non-empty options" { $global:FakeInstallStatus = 'Ok' InModuleScope WingetBatch { Mock Get-Command { Get-Command Test-FakeInstall } -ParameterFilter { $Name -like 'Microsoft.WinGet.Client\*' } $r = Invoke-WingetPackageAction -Action Install -Id 'Git.Git' -Version 'latest' -Source 'winget' ` -Options @{ Mode = 'Silent'; Scope = $null; Force = $false; Override = '--foo' } $r.Succeeded | Should -BeTrue } $global:FakeInstallArgs['MatchOption'] | Should -Be 'EqualsCaseInsensitive' $global:FakeInstallArgs['Source'] | Should -Be 'winget' $global:FakeInstallArgs['Mode'] | Should -Be 'Silent' $global:FakeInstallArgs.ContainsKey('Version') | Should -BeFalse # 'latest' is not a real version $global:FakeInstallArgs.ContainsKey('Scope') | Should -BeFalse # empty values are dropped $global:FakeInstallArgs.ContainsKey('Force') | Should -BeFalse # false switches are dropped $global:FakeInstallArgs.ContainsKey('Override') | Should -BeFalse # unsupported by this cmdlet } It "returns a failure result when the installer fails" { $global:FakeInstallStatus = 'InstallError' InModuleScope WingetBatch { Mock Get-Command { Get-Command Test-FakeInstall } -ParameterFilter { $Name -like 'Microsoft.WinGet.Client\*' } (Invoke-WingetPackageAction -Action Install -Id 'Git.Git').Succeeded | Should -BeFalse } } } Describe "Get-WingetYamlValue" { It "reads plain, quoted and block values" { InModuleScope WingetBatch { $yaml = @" PackageIdentifier: Git.Git License: 'GPL-2.0' ShortDescription: "A VCS" ReleaseNotes: |- Line one Line two Tags: - git "@ Get-WingetYamlValue -Yaml $yaml -Key 'PackageIdentifier' | Should -Be 'Git.Git' Get-WingetYamlValue -Yaml $yaml -Key 'License' | Should -Be 'GPL-2.0' Get-WingetYamlValue -Yaml $yaml -Key 'ShortDescription' | Should -Be 'A VCS' Get-WingetYamlValue -Yaml $yaml -Key 'ReleaseNotes' | Should -Be "Line one`nLine two" Get-WingetYamlValue -Yaml $yaml -Key 'Missing' | Should -BeNullOrEmpty } } } Describe "Parse-WingetShowOutput multi-line fields" { It "reads Description, Tags and Release Notes written on the lines below their label" { InModuleScope WingetBatch { $out = @" Found Git [Git.Git] Version: 2.55.0.3 Description: Git is a free VCS. It is fast. Tags: git vcs Release Notes: - Fixes: a crash Installer: Installer Type: inno "@ $i = Parse-WingetShowOutput -Output $out -PackageId 'Git.Git' $i.Description | Should -Be 'Git is a free VCS. It is fast.' $i.Tags | Should -Be @('git', 'vcs') $i.ReleaseNotes | Should -Be '- Fixes: a crash' $i.Installer | Should -Be 'inno' $i.Version | Should -Be '2.55.0.3' } } } Describe "Get-WingetPkgsPackagePath" { It "maps IDs to winget-pkgs folders" { InModuleScope WingetBatch { Get-WingetPkgsPackagePath -PackageId 'Python.Python.3.13' | Should -Be 'manifests/p/Python/Python/3/13' } } } Describe "Config read-modify-write" { It "keeps existing keys when another setting is saved" { InModuleScope WingetBatch { Mock Get-WingetBatchConfigDir { Join-Path $TestDrive 'cfg' } $c = Get-WingetBatchConfigData $c['webhook_discord'] = 'https://example.invalid/hook' Save-WingetBatchConfigData -Config $c Set-WingetBatchConfig -SearchMatchOption EqualsCaseInsensitive 6>$null Send-WingetWebhook -Platform Slack -SaveConfig -WebhookUrl 'https://example.invalid/slack' 6>$null $after = Get-WingetBatchConfigData $after['webhook_discord'] | Should -Be 'https://example.invalid/hook' $after['webhook_slack'] | Should -Be 'https://example.invalid/slack' $after['SearchMatchOption'] | Should -Be 'EqualsCaseInsensitive' } } } Describe "Command contracts" { It "Send-WingetWebhook -SaveConfig does not require -Event" { InModuleScope WingetBatch { Mock Get-WingetBatchConfigData { @{} } Mock Save-WingetBatchConfigData { } Mock Invoke-RestMethod { } Send-WingetWebhook -Platform Discord -SaveConfig -WebhookUrl 'https://example.invalid/x' 6>$null Should -Invoke Save-WingetBatchConfigData -Times 1 Should -Invoke Invoke-RestMethod -Times 0 } } It "Test-WingetCompliance -NewPolicy accepts -PolicyPath" { $set = (Get-Command Test-WingetCompliance).ParameterSets | Where-Object Name -eq 'Generate' $set.Parameters.Name | Should -Contain 'PolicyPath' } It "Get-WingetMachineState reports a missing manifest instead of failing to find its helpers" { { Get-WingetMachineState -Compare -Path (Join-Path $TestDrive 'nope.json') -ErrorAction Stop } | Should -Throw '*State manifest not found*' } It "Get-WingetUpdates exposes -ListOnly for scripts" { (Get-Command Get-WingetUpdates).Parameters.ContainsKey('ListOnly') | Should -BeTrue } } Describe "Source hygiene" { BeforeAll { $root = Join-Path $PSScriptRoot ".." $script:files = Get-ChildItem (Join-Path $root 'Public'), (Join-Path $root 'Private') -Filter *.ps1 } It "every source file parses" { foreach ($f in $files) { $errors = $null [void][System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$errors) $errors | Should -BeNullOrEmpty -Because $f.Name } } It "defines each function once per file, and only functions at top level" { # A bad edit once left an old copy of a function after the new one; PowerShell # silently keeps the last definition, so the fix never ran. foreach ($f in $files) { $ast = [System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$null) $top = @($ast.EndBlock.Statements) $functions = @($top | Where-Object { $_ -is [System.Management.Automation.Language.FunctionDefinitionAst] }) ($top.Count - $functions.Count) | Should -Be 0 -Because "$($f.Name) should only define functions" @($functions.Name | Group-Object | Where-Object Count -gt 1) | Should -BeNullOrEmpty -Because $f.Name if ($f.Directory.Name -eq 'Public') { $functions.Name | Should -Contain $f.BaseName } } } It "never uses Get-Date -ToString (not a real parameter)" { $hits = $files | Select-String -Pattern 'Get-Date -ToString' $hits | Should -BeNullOrEmpty } It "calls WinGet cmdlets by module-qualified name (other modules shadow them)" { # Inspect actual command invocations in the syntax tree, not help text or strings $hits = foreach ($f in $files) { $ast = [System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$null) $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.CommandAst] }, $true) | Where-Object { $_.GetCommandName() -match '^(Get|Find|Install|Update|Uninstall|Export)-WinGet(Package|Version)$' } | ForEach-Object { "$($f.Name):$($_.Extent.StartLineNumber)" } } $hits | Should -BeNullOrEmpty } It "keeps release notes under the PowerShell Gallery's 10,600 character limit" { $manifest = Import-PowerShellDataFile (Join-Path $PSScriptRoot ".." "WingetBatch.psd1") $manifest.PrivateData.PSData.ReleaseNotes.Length | Should -BeLessOrEqual 10600 } It "declares the PowerShell version its dependencies actually need" { $manifest = Import-PowerShellDataFile (Join-Path $PSScriptRoot ".." "WingetBatch.psd1") [version]$manifest.PowerShellVersion | Should -BeGreaterOrEqual ([version]'7.4') } } |