From 0f03c104da37ddedd465b1c72d6526529b5166fe Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:44:21 -0700 Subject: [PATCH] Fix backwards compatability with output objects --- Test/V2ObjectCompatibility.Tests.ps1 | 283 +++++++++++++++++++++++++++ src/PowerShellGet.psm1 | 105 ++++++++-- 2 files changed, 371 insertions(+), 17 deletions(-) create mode 100644 Test/V2ObjectCompatibility.Tests.ps1 diff --git a/Test/V2ObjectCompatibility.Tests.ps1 b/Test/V2ObjectCompatibility.Tests.ps1 new file mode 100644 index 0000000..6fa918b --- /dev/null +++ b/Test/V2ObjectCompatibility.Tests.ps1 @@ -0,0 +1,283 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Write-Verbose -Verbose -Message "PowerShellGet version currently loaded: $($(Get-Module powershellget).Version)" + +Describe "Test CompatPowerShellGet: V2 Object Property Compatibility" -tags 'CI' { + + BeforeAll { + $PSGalleryName = 'PSGallery' + $testModuleName = "testmodule99" + $testScriptName = "test_script" + + $v2RepositoryProperties = @( + 'Name', + 'SourceLocation', + 'Trusted', + 'Registered', + 'InstallationPolicy', + 'PackageManagementProvider', + 'PublishLocation', + 'ScriptSourceLocation', + 'ScriptPublishLocation', + 'ProviderOptions' + ) + + # Properties that exist natively on PSResourceInfo plus our v2 additions + $v2ResourceProperties = @( + 'Name', + 'Version', + 'Type', + 'Description', + 'Author', + 'CompanyName', + 'Copyright', + 'PublishedDate', + 'LicenseUri', + 'ProjectUri', + 'IconUri', + 'Tags', + 'Includes', + 'ReleaseNotes', + 'Dependencies', + 'Repository', + 'RepositorySourceLocation', + 'PackageManagementProvider' + ) + + # The two properties we add for v2 compat + $v2AddedProperties = @( + 'RepositorySourceLocation', + 'PackageManagementProvider' + ) + } + + Context "Get-PSRepository output" { + It "should have all v2 properties" { + $res = Get-PSRepository -Name $PSGalleryName + $res | Should -Not -BeNullOrEmpty + foreach ($prop in $v2RepositoryProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + } + + It "should have correct InstallationPolicy value" { + $res = Get-PSRepository -Name $PSGalleryName + $res.InstallationPolicy | Should -BeIn @('Trusted', 'Untrusted') + } + + It "should have InstallationPolicy consistent with Trusted" { + $res = Get-PSRepository -Name $PSGalleryName + if ($res.Trusted) { + $res.InstallationPolicy | Should -Be 'Trusted' + } else { + $res.InstallationPolicy | Should -Be 'Untrusted' + } + } + + It "should have Registered set to true" { + $res = Get-PSRepository -Name $PSGalleryName + $res.Registered | Should -BeTrue + } + + It "should have PackageManagementProvider set to NuGet" { + $res = Get-PSRepository -Name $PSGalleryName + $res.PackageManagementProvider | Should -Be 'NuGet' + } + + It "should have SourceLocation populated" { + $res = Get-PSRepository -Name $PSGalleryName + $res.SourceLocation | Should -Not -BeNullOrEmpty + } + + It "should have PublishLocation populated" { + $res = Get-PSRepository -Name $PSGalleryName + $res.PublishLocation | Should -Not -BeNullOrEmpty + } + + It "should have ProviderOptions as hashtable" { + $res = Get-PSRepository -Name $PSGalleryName + $res.ProviderOptions | Should -BeOfType [hashtable] + } + + It "should display as table with Name, InstallationPolicy, SourceLocation columns" { + $res = Get-PSRepository -Name $PSGalleryName + $res.PSObject.TypeNames | Should -Contain 'Microsoft.PowerShell.Commands.PSRepositoryInfo' + } + + It "should have correct properties for locally registered repo" { + $tmpDir = Join-Path -Path $TestDrive -ChildPath "v2TestDir" + New-Item -Path $tmpDir -ItemType Directory -Force | Out-Null + Register-PSResourceRepository -Name "v2TestRepo" -Uri $tmpDir + try { + $res = Get-PSRepository -Name "v2TestRepo" + $res | Should -Not -BeNullOrEmpty + foreach ($prop in $v2RepositoryProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + $res.InstallationPolicy | Should -Be 'Untrusted' + $res.Registered | Should -BeTrue + $res.PackageManagementProvider | Should -Be 'NuGet' + } finally { + Unregister-PSResourceRepository -Name "v2TestRepo" + Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + + Context "Register-PSRepository output" { + It "registered repo should be visible via Get-PSRepository with v2 properties" { + $tmpDir = Join-Path -Path $TestDrive -ChildPath "v2RegTestDir" + New-Item -Path $tmpDir -ItemType Directory -Force | Out-Null + Unregister-PSResourceRepository -Name "v2RegTestRepo" -ErrorAction SilentlyContinue + Register-PSRepository -Name "v2RegTestRepo" -SourceLocation $tmpDir + try { + $res = Get-PSRepository -Name "v2RegTestRepo" + $res | Should -Not -BeNullOrEmpty + foreach ($prop in $v2RepositoryProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + } finally { + Unregister-PSRepository -Name "v2RegTestRepo" + Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + + Context "Find-Module output" { + It "should have all v2 resource properties" { + $res = Find-Module -Name $testModuleName -Repository $PSGalleryName + $res | Should -Not -BeNullOrEmpty + foreach ($prop in $v2ResourceProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + } + + It "should have PackageManagementProvider set to NuGet" { + $res = Find-Module -Name $testModuleName -Repository $PSGalleryName + $res.PackageManagementProvider | Should -Be 'NuGet' + } + + It "should have RepositorySourceLocation as string" { + $res = Find-Module -Name $testModuleName -Repository $PSGalleryName + $res.RepositorySourceLocation | Should -BeOfType [string] + } + } + + Context "Find-Script output" { + It "should have all v2 resource properties" { + $res = Find-Script -Name $testScriptName -Repository $PSGalleryName + $res | Should -Not -BeNullOrEmpty + foreach ($prop in $v2ResourceProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + } + + It "should have PackageManagementProvider set to NuGet" { + $res = Find-Script -Name $testScriptName -Repository $PSGalleryName + $res.PackageManagementProvider | Should -Be 'NuGet' + } + } + + Context "Find-Command output" { + It "should have v2-added properties when results are returned" { + $res = Find-Command -Name "Get-TargetResource" -Repository $PSGalleryName -ErrorAction SilentlyContinue + if ($res) { + foreach ($prop in $v2AddedProperties) { + $res[0].PSObject.Properties.Name | Should -Contain $prop + } + $res[0].PackageManagementProvider | Should -Be 'NuGet' + } else { + Set-ItResult -Skipped -Because "No results returned from Find-Command" + } + } + } + + Context "Find-DscResource output" { + It "should have v2-added properties when results are returned" { + $res = Find-DscResource -Name "SystemLocale" -Repository $PSGalleryName -ErrorAction SilentlyContinue + if ($res) { + foreach ($prop in $v2AddedProperties) { + $res[0].PSObject.Properties.Name | Should -Contain $prop + } + $res[0].PackageManagementProvider | Should -Be 'NuGet' + } else { + Set-ItResult -Skipped -Because "No results returned from Find-DscResource" + } + } + } + + Context "Get-InstalledModule output" { + It "should have all v2 resource properties" { + $res = Get-InstalledModule -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($res) { + foreach ($prop in $v2ResourceProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + $res.PackageManagementProvider | Should -Be 'NuGet' + $res.RepositorySourceLocation | Should -BeOfType [string] + } else { + Set-ItResult -Skipped -Because "No installed modules found" + } + } + } + + Context "Get-InstalledScript output" { + It "should have all v2 resource properties" { + $res = Get-InstalledScript -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($res) { + foreach ($prop in $v2ResourceProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + $res.PackageManagementProvider | Should -Be 'NuGet' + } else { + Set-ItResult -Skipped -Because "No installed scripts found" + } + } + } + + Context "Install-Module output with -PassThru" { + It "should have v2 resource properties with -PassThru" -Skip { + # Skipped by default: installs a module. Unskip for integration testing. + $res = Install-Module -Name $testModuleName -Repository $PSGalleryName -PassThru -Scope CurrentUser -Force + if ($res) { + foreach ($prop in $v2ResourceProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + $res.PackageManagementProvider | Should -Be 'NuGet' + } + Uninstall-Module -Name $testModuleName -ErrorAction SilentlyContinue + } + } + + Context "Update-Module output with -PassThru" { + It "should have v2 resource properties with -PassThru" -Skip { + # Skipped by default: modifies installed modules. Unskip for integration testing. + $res = Update-Module -Name $testModuleName -PassThru -Force -ErrorAction SilentlyContinue + if ($res) { + foreach ($prop in $v2ResourceProperties) { + $res.PSObject.Properties.Name | Should -Contain $prop + } + $res.PackageManagementProvider | Should -Be 'NuGet' + } + } + } + + Context "Cross-cmdlet property consistency" { + It "Get-PSRepository and Find-Module should both have PackageManagementProvider" { + $repo = Get-PSRepository -Name $PSGalleryName + $mod = Find-Module -Name $testModuleName -Repository $PSGalleryName + + $repo.PackageManagementProvider | Should -Be 'NuGet' + $mod.PackageManagementProvider | Should -Be 'NuGet' + } + + It "Get-PSRepository SourceLocation should match Find-Module RepositorySourceLocation" { + $repo = Get-PSRepository -Name $PSGalleryName + $mod = Find-Module -Name $testModuleName -Repository $PSGalleryName + + $repo.SourceLocation | Should -Not -BeNullOrEmpty + $mod.PSObject.Properties.Name | Should -Contain 'RepositorySourceLocation' + } + } +} diff --git a/src/PowerShellGet.psm1 b/src/PowerShellGet.psm1 index c50b024..28480ec 100644 --- a/src/PowerShellGet.psm1 +++ b/src/PowerShellGet.psm1 @@ -143,6 +143,73 @@ function Convert-VersionParamaters } } +# Adds v2-compatible properties to PSResourceRepository objects +function Add-V2RepositoryProperties { + [CmdletBinding()] + param([Parameter(ValueFromPipeline=$true)]$InputObject) + process { + $sourceLocation = if ($InputObject.Uri) { $InputObject.Uri.ToString() } else { '' } + $installPolicy = if ($InputObject.Trusted) { 'Trusted' } else { 'Untrusted' } + $publishLocation = '' + $scriptSourceLocation = '' + $scriptPublishLocation = '' + if ($sourceLocation) { + $baseUri = $sourceLocation.TrimEnd('/') + $publishLocation = "$baseUri/package/" + $scriptSourceLocation = "$baseUri/items/psscript" + $scriptPublishLocation = "$baseUri/package/" + } + [PSCustomObject]@{ + PSTypeName = 'Microsoft.PowerShell.Commands.PSRepositoryInfo' + Name = $InputObject.Name + SourceLocation = $sourceLocation + Trusted = $InputObject.Trusted + Registered = $true + InstallationPolicy = $installPolicy + PackageManagementProvider = 'NuGet' + PublishLocation = $publishLocation + ScriptSourceLocation = $scriptSourceLocation + ScriptPublishLocation = $scriptPublishLocation + ProviderOptions = @{} + } + } +} + +# Adds v2-compatible properties to PSResourceInfo objects +function Add-V2ResourceProperties { + [CmdletBinding()] + param([Parameter(ValueFromPipeline=$true)]$InputObject) + process { + $repoUrl = if ($InputObject.RepositorySourceUrl) { $InputObject.RepositorySourceUrl.ToString() } else { '' } + [PSCustomObject]@{ + PSTypeName = 'Microsoft.PowerShell.Commands.PSRepositoryItemInfo' + Name = $InputObject.Name + Version = $InputObject.Version + Type = $InputObject.Type + Description = $InputObject.Description + Author = $InputObject.Author + CompanyName = $InputObject.CompanyName + Copyright = $InputObject.Copyright + PublishedDate = $InputObject.PublishedDate + InstalledDate = $InputObject.InstalledDate + UpdatedDate = $InputObject.UpdatedDate + LicenseUri = $InputObject.LicenseUri + ProjectUri = $InputObject.ProjectUri + IconUri = $InputObject.IconUri + Tags = $InputObject.Tags + Includes = $InputObject.Includes + PowerShellGetFormatVersion = $InputObject.PowerShellGetFormatVersion + ReleaseNotes = $InputObject.ReleaseNotes + Dependencies = $InputObject.Dependencies + RepositorySourceLocation = $repoUrl + Repository = $InputObject.Repository + PackageManagementProvider = 'NuGet' + AdditionalMetadata = $InputObject.AdditionalMetadata + InstalledLocation = $InputObject.InstalledLocation + } + } +} + #### #### # Proxy functions @@ -234,7 +301,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Find-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -360,7 +427,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Find-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -517,7 +584,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Find-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -727,7 +794,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Find-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -826,7 +893,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-InstalledPSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -922,7 +989,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-InstalledPSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -987,7 +1054,7 @@ param( } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Get-PSResourceRepository', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2RepositoryProperties } # Set internal hook for being invoked from Compat module try { @@ -1136,7 +1203,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Install-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -1279,7 +1346,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Install-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -1799,7 +1866,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Register-PSResourceRepository', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2RepositoryProperties } # Set internal hook for being invoked from Compat module try { @@ -1951,7 +2018,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Save-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -2102,7 +2169,7 @@ param( if ( $PSBoundParameters['AcceptLicense'] ) { $null = $PSBoundParameters.Remove('AcceptLicense') } $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Save-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -2229,7 +2296,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Set-PSResourceRepository', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2RepositoryProperties } # Set internal hook for being invoked from Compat module try { @@ -2417,7 +2484,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Uninstall-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -2524,7 +2591,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Uninstall-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -2714,7 +2781,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Update-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -2832,7 +2899,7 @@ param( # END PARAMETER MAP $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Update-PSResource', [System.Management.Automation.CommandTypes]::Cmdlet) - $scriptCmd = {& $wrappedCmd @PSBoundParameters } + $scriptCmd = {& $wrappedCmd @PSBoundParameters | Add-V2ResourceProperties } # Set internal hook for being invoked from Compat module try { @@ -3216,4 +3283,8 @@ $aliasesToExport = @(' 'upmo' ) +# Configure default display to match v2 formatting +Update-TypeData -TypeName 'Microsoft.PowerShell.Commands.PSRepositoryInfo' -DefaultDisplayPropertySet 'Name','InstallationPolicy','SourceLocation' -Force +Update-TypeData -TypeName 'Microsoft.PowerShell.Commands.PSRepositoryItemInfo' -DefaultDisplayPropertySet 'Name','Version','Type','Repository','Description' -Force + export-ModuleMember -Function $functionsToExport -Alias $aliasesToExport