diff --git a/.config/tsaoptions.json b/.config/tsaoptions.json index 692eaec1f..f1c0afb59 100644 --- a/.config/tsaoptions.json +++ b/.config/tsaoptions.json @@ -1,7 +1,7 @@ { "instanceUrl": "https://msazure.visualstudio.com", "projectName": "One", - "areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell\\PowerShell Core", + "areaPath": "One\\MGMT\\Compute\\Powershell\\Powershell\\PowerShell Core\\PSResourceGet", "notificationAliases": [ "adityap@microsoft.com", "americks@microsoft.com", diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1b53fe53a..ff70521d0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,4 +4,4 @@ # the repo. Unless a later match takes precedence, # the following owners will be requested for # review when someone opens a pull request. -* @anamnavi @alerickson @adityapatwardhan @SydneyhSmith +* @anamnavi @alerickson @adityapatwardhan @SydneyhSmith @shammu1 diff --git a/.pipelines/PSResourceGet-Official.yml b/.pipelines/PSResourceGet-Official.yml index cc51e2e78..e4ea3ba1e 100644 --- a/.pipelines/PSResourceGet-Official.yml +++ b/.pipelines/PSResourceGet-Official.yml @@ -1,14 +1,5 @@ -################################################################################# -# OneBranch Pipelines # -# This pipeline was created by EasyStart from a sample located at: # -# https://aka.ms/obpipelines/easystart/samples # -# Documentation: https://aka.ms/obpipelines # -# Yaml Schema: https://aka.ms/obpipelines/yaml/schema # -# Retail Tasks: https://aka.ms/obpipelines/tasks # -# Support: https://aka.ms/onebranchsup # -################################################################################# name: PSResourceGet-Release-$(Build.BuildId) -trigger: none # https://aka.ms/obpipelines/triggers +trigger: none pr: branches: include: @@ -36,7 +27,7 @@ resources: ref: refs/heads/main extends: - template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates # https://aka.ms/obpipelines/templates + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates parameters: featureFlags: WindowsHostVersion: '1ESWindows2022' @@ -49,18 +40,16 @@ extends: enabled: true packageName: Microsoft.PowerShell.PSResourceGet codeql: + tsaEnabled: true compiled: enabled: true - asyncSdl: # https://aka.ms/obpipelines/asyncsdl + credscan: enabled: true - forStages: [stagebuild] - credscan: - enabled: true - scanFolder: $(Build.SourcesDirectory)\PSResourceGet - binskim: - enabled: true - apiscan: - enabled: false + scanFolder: $(Build.SourcesDirectory)\PSResourceGet + binskim: + enabled: true + apiscan: + enabled: false stages: - stage: stagebuild @@ -122,7 +111,7 @@ extends: ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. inputs: Enabled: true - AnalyzeInPipeline: true + AnalyzeInPipeline: false Language: csharp # this is installing .NET @@ -220,6 +209,8 @@ extends: type: windows steps: - checkout: self + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - pwsh: | if (-not (Test-Path $(repoRoot)/.config/tsaoptions.json)) { @@ -227,29 +218,48 @@ extends: throw "tsaoptions.json does not exist under $(repoRoot)/.config" } displayName: Test if tsaoptions.json exists + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - task: DownloadPipelineArtifact@2 displayName: 'Download build files' inputs: targetPath: $(signOutPath) artifact: drop_stagebuild_jobbuild + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - pwsh: | Set-Location "$(signOutPath)" Write-Host "Contents of signOutPath:" Get-ChildItem $(signOutPath) -Recurse displayName: Capture artifacts directory structure + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - pwsh: | # This need to be done before set-location so the module from PSHome is loaded Import-Module -Name Microsoft.PowerShell.PSResourceGet -Force Set-Location "$(signOutPath)\Microsoft.PowerShell.PSResourceGet" - $null = New-Item -ItemType Directory -Path "$(signOutPath)\PublishedNupkg" -Force + $publishPath = Join-Path $(signOutPath) -ChildPath 'PublishedNupkg' + + $null = New-Item -ItemType Directory -Path $publishPath -Force - Register-PSResourceRepository -Name 'localRepo' -Uri "$(signOutPath)\PublishedNupkg" + Register-PSResourceRepository -Name 'localRepo' -Uri $publishPath Publish-PSResource -Path "$(signOutPath)\Microsoft.PowerShell.PSResourceGet" -Repository 'localRepo' -Verbose + Write-Output "##vso[task.setvariable variable=publishPath]$publishPath" displayName: Create nupkg for publishing + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. + + - pwsh: | + Set-Location '$(publishPath)' + Write-Host "Contents of signOutPath:" + Get-ChildItem '$(publishPath)' -Recurse + displayName: Find Nupkg Pre Signing + env: + ob_restore_phase: true # Set ob_restore_phase to run this step before '🔒 Setup Signing' step. - task: onebranch.pipeline.signing@1 displayName: Sign nupkg @@ -257,18 +267,18 @@ extends: command: 'sign' signing_profile: external_distribution files_to_sign: '**\*.nupkg' - search_root: "$(signOutPath)\PublishedNupkg" + search_root: '$(publishPath)' - pwsh: | - Set-Location "$(signOutPath)\PublishedNupkg" + Set-Location '$(publishPath)' Write-Host "Contents of signOutPath:" - Get-ChildItem "$(signOutPath)" -Recurse - displayName: Find Nupkg + Get-ChildItem '$(publishPath)' -Recurse + displayName: Find Nupkg Post Signing - task: CopyFiles@2 displayName: "Copy nupkg to ob_outputDirectory - '$(ob_outputDirectory)'" inputs: - Contents: $(signOutPath)\PublishedNupkg\Microsoft.PowerShell.PSResourceGet.*.nupkg + Contents: $(publishPath)\Microsoft.PowerShell.PSResourceGet.*.nupkg TargetFolder: $(ob_outputDirectory) - pwsh: | diff --git a/CHANGELOG/1.2.md b/CHANGELOG/1.2.md new file mode 100644 index 000000000..35912468c --- /dev/null +++ b/CHANGELOG/1.2.md @@ -0,0 +1,4 @@ +# 1.2 Changelog + +## [1.2.0](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-rc3...v1.2.0) - 2026-03-10 + diff --git a/CHANGELOG/preview.md b/CHANGELOG/preview.md index a436c10bb..22d2e5e8e 100644 --- a/CHANGELOG/preview.md +++ b/CHANGELOG/preview.md @@ -1,5 +1,51 @@ # Preview Changelog +## [1.2.0-rc3](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-rc2..v1.2.0-rc3) - 2026-02-06 + +## Bug fix +- Packages that depend on a specific version should search for the dependency with NormalizedVersion (#1941) + +## [1.2.0-rc2](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-rc1..v1.2.0-rc2) - 2026-02-05 + +## Bug fix +- For packages that depend on a specific version, use an exact version instead of a version range. (#1937) + +## [1.2.0-rc1](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-preview5..v1.2.0-rc1) - 2026-01-14 + +## Bug fix +- `WhatIf` parameter should respect provided value instead of simply checking presence (#1925) + +## [1.2.0-preview5](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-preview4..v1.2.0-preview5) - 2025-12-05 + +### New Features +- Add `Reset-PSResourceRepository` cmdlet to recover from corrupted repository store (#1895) +- Improve performance of `ContainerRegistry` repositories by caching token (#1920) + +## Bug fix +- Ensure `Update-PSResource` does not re-install dependency packages which already satisfy dependency criteria (#1919) +- Retrieve non-anonymous access token when publishing to ACR (#1918) +- Filter out path separators when passing in package names as a parameter for any cmdlet (#1916) +- Respect `TrustRepository` parameter when using `-RequiredResource` with `Install-PSResource` (#1910) +- Fix bug with 'PSModuleInfo' property deserialization when validating module manifest (#1909) +- Prevent users from setting ApiVersion to 'Unknown' in `Set-PSResourceRepository` and `Register-PSResourceRepository` (#1892) + +## [1.2.0-preview4](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-preview3..v1.2.0-preview4) - 2025-11-04 + +## Bug fix + +- Fix typos in numerous files (#1875 Thanks @SamErde!) +- MAR fails to parse RequiredVersion for dependencies (#1876 Thanks @o-l-a-v!) +- Get-InstalledPSResource -Path don't throw if no subdirectories were found (#1877 Thanks @o-l-a-v!) +- Handle boolean correctly in RequiredResourceFile for prerelease key (#1843 Thanks @o-l-a-v!) +- Fix CodeQL configuration (#1886) +- Add cmdlet aliases: gres, usres, and svres (#1888) +- Add warning when AuthenticodeCheck is used on non-Windows platforms (#1891) +- Fix Compress-PSResource ignoring .gitkeep and other dotfiles (#1889) +- Add CodeQL suppression for ContainerRegistryServerAPICalls (#1897) +- Fix broken Install-PSResource test with warning condition incorrect (#1899) +- Uninstall-PSResource should not fail silently when resource was not found or prerelease criteria not met (#1898) +- Uninstall-PSResource should delete subdirectories without Access Denied error on OneDrive (#1860) + ## [1.2.0-preview3](https://github.com/PowerShell/PSResourceGet/compare/v1.2.0-preview2..v1.2.0-preview3) - 2025-09-12 ### New Features diff --git a/global.json b/global.json index 100631a86..40cb45844 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.414" + "version": "8.0.419" } } diff --git a/src/Microsoft.PowerShell.PSResourceGet.psd1 b/src/Microsoft.PowerShell.PSResourceGet.psd1 index fad665d1c..17c341571 100644 --- a/src/Microsoft.PowerShell.PSResourceGet.psd1 +++ b/src/Microsoft.PowerShell.PSResourceGet.psd1 @@ -23,6 +23,7 @@ 'Get-PSScriptFileInfo', 'Install-PSResource', 'Register-PSResourceRepository', + 'Reset-PSResourceRepository', 'Save-PSResource', 'Set-PSResourceRepository', 'New-PSScriptFileInfo', @@ -41,12 +42,15 @@ AliasesToExport = @( 'Get-PSResource', 'fdres', + 'gres', 'isres', 'pbres', - 'udres') + 'svres', + 'udres', + 'usres') PrivateData = @{ PSData = @{ - Prerelease = 'preview3' + # Prerelease = '' Tags = @('PackageManagement', 'PSEdition_Desktop', 'PSEdition_Core', @@ -56,6 +60,54 @@ ProjectUri = 'https://go.microsoft.com/fwlink/?LinkId=828955' LicenseUri = 'https://go.microsoft.com/fwlink/?LinkId=829061' ReleaseNotes = @' +## 1.2.0 + +## 1.2.0-rc3 + +## Bug fix +- Packages that depend on a specific version should search for the dependency with NormalizedVersion (#1941) + +## 1.2.0-rc2 + +## Bug fix +- For packages with dependency on a specific version use specific version instead of version range (#1937) + +## 1.2.0-rc1 + +## Bug fix +- `WhatIf` parameter should respect provided value instead of simply checking presence (#1925) + +## 1.2.0-preview5 + +### New Features +- Add `Reset-PSResourceRepository` cmdlet to recover from corrupted repository store (#1895) +- Improve performance of `ContainerRegistry` repositories by caching token (#1920) + +## Bug fix +- Ensure `Update-PSResource` does not re-install dependency packages which already satisfy dependency criteria (#1919) +- Retrieve non-anonymous access token when publishing to ACR (#1918) +- Filter out path separators when passing in package names as a parameter for any cmdlet (#1916) +- Respect `TrustRepository` parameter when using `-RequiredResource` with `Install-PSResource` (#1910) +- Fix bug with 'PSModuleInfo' property deserialization when validating module manifest (#1909) +- Prevent users from setting ApiVersion to 'Unknown' in `Set-PSResourceRepository` and `Register-PSResourceRepository` (#1892) + +## 1.2.0-preview4 + +## Bug fix + +- Fix typos in numerous files (#1875 Thanks @SamErde!) +- MAR fails to parse RequiredVersion for dependencies (#1876 Thanks @o-l-a-v!) +- Get-InstalledPSResource -Path don't throw if no subdirectories were found (#1877 Thanks @o-l-a-v!) +- Handle boolean correctly in RequiredResourceFile for prerelease key (#1843 Thanks @o-l-a-v!) +- Fix CodeQL configuration (#1886) +- Add cmdlet aliases: gres, usres, and svres (#1888) +- Add warning when AuthenticodeCheck is used on non-Windows platforms (#1891) +- Fix Compress-PSResource ignoring .gitkeep and other dotfiles (#1889) +- Add CodeQL suppression for ContainerRegistryServerAPICalls (#1897) +- Fix broken Install-PSResource test with warning condition incorrect (#1899) +- Uninstall-PSResource should not fail silently when resource was not found or prerelease criteria not met (#1898) +- Uninstall-PSResource should delete subdirectories without Access Denied error on OneDrive (#1860) + ## 1.2.0-preview3 ### New Features @@ -87,164 +139,6 @@ - Improvements in `ContainerRegistry` repositories in listing repository catalog (#1831) - Wildcard attribute added to `-Repository` parameter of `Install-PSResource` (#1808) -## 1.1.1 - -### Bug Fix -- Bugfix to retrieve all metadata properties when finding a PSResource from a ContainerRegistry repository (#1799) -- Update README.md (#1798) -- Use authentication challenge for unauthenticated ContainerRegistry repository (#1797) -- Bugfix for Install-PSResource with varying digit version against ContainerRegistry repository (#1796) -- Bugfix for updating ContainerRegistry dependency parsing logic to account for AzPreview package (#1792) -- Add wildcard support for MAR repository for FindAll and FindByName (#1786) -- Bugfix for nuspec dependency version range calculation for RequiredModules (#1784) - -## 1.1.0 - -### Bug Fix -- Bugfix for publishing .nupkg file to ContainerRegistry repository (#1763) -- Bugfix for PMPs like Artifactory needing modified filter query parameter to proxy upstream (#1761) -- Bugfix for ContainerRegistry repository to parse out dependencies from metadata (#1766) -- Bugfix for Install-PSResource Null pointer occurring when package is present only in upstream feed in ADO (#1760) -- Bugfix for local repository casing issue on Linux (#1750) -- Update README.md (#1759) -- Bug fix for case sensitive License.txt when RequireLicense is specified (#1757) -- Bug fix for broken -Quiet parameter for Save-PSResource (#1745) - -## 1.1.0-rc3 - -### Bug Fix -- Include missing commits - -## 1.1.0-RC2 - -### New Features -- Full Microsoft Artifact Registry integration (#1741) - -### Bug Fixes - -- Update to use OCI v2 APIs for Container Registry (#1737) -- Bug fixes for finding and installing from local repositories on Linux machines (#1738) -- Bug fix for finding package name with 4 part version from local repositories (#1739) - -## 1.1.0-RC1 - -### New Features - -- Group Policy configurations for enabling or disabling PSResource repositories (#1730) - -### Bug Fixes - -- Fix packaging name matching when searching in local repositories (#1731) -- `Compress-PSResource` `-PassThru` now passes `FileInfo` instead of string (#1720) -- Fix for `Compress-PSResource` not properly compressing scripts (#1719) -- Add `AcceptLicense` to Save-PSResource (#1718 Thanks @o-l-a-v!) -- Better support for NuGet v2 feeds (#1713 Thanks @o-l-a-v!) -- Better handling of `-WhatIf` support in `Install-PSResource` (#1531 Thanks @o-l-a-v!) -- Fix for some nupkgs failing to extract due to empty directories (#1707 Thanks @o-l-a-v!) -- Fix for searching for `-Name *` in `Find-PSResource` (#1706 Thanks @o-l-a-v!) - -## 1.1.0-preview2 - -### New Features - -- New cmdlet `Compress-PSResource` which packs a package into a .nupkg and saves it to the file system (#1682, #1702) -- New `-Nupkg` parameter for `Publish-PSResource` which pushes pushes a .nupkg to a repository (#1682) -- New `-ModulePrefix` parameter for `Publish-PSResource` which adds a prefix to a module name for container registry repositories to add a module prefix.This is only used for publishing and is not part of metadata. MAR will drop the prefix when syndicating from ACR to MAR (#1694) - -### Bug Fixes - -- Add prerelease string when NormalizedVersion doesn't exist, but prerelease string does (#1681 Thanks @sean-r-williams) -- Add retry logic when deleting files (#1667 Thanks @o-l-a-v!) -- Fix broken PAT token use (#1672) -- Updated error messaging for authenticode signature failures (#1701) - -## 1.1.0-preview1 - -### New Features - -- Support for Azure Container Registries (#1495, #1497-#1499, #1501, #1502, #1505, #1522, #1545, #1548, #1550, #1554, #1560, #1567, -#1573, #1576, #1587, #1588, #1589, #1594, #1598, #1600, #1602, #1604, #1615) - -### Bug Fixes - -- Fix incorrect request URL when installing resources from ADO (#1597 Thanks @anytonyoni!) -- Fix for swallowed exceptions (#1569) -- Fix for PSResourceGet not working in Constrained Language Mode (#1564) - -## 1.0.6 - -- Bump System.Text.Json to 8.0.5 - -## [1.0.5](https://github.com/PowerShell/PSResourceGet/compare/v1.0.4.1...v1.0.5) - 2024-05-13 - -### Bug Fixes -- Update `nuget.config` to use PowerShell packages feed (#1649) -- Refactor V2ServerAPICalls and NuGetServerAPICalls to use object-oriented query/filter builder (#1645 Thanks @sean-r-williams!) -- Fix unnecessary `and` for version globbing in V2ServerAPICalls (#1644 Thanks again @sean-r-williams!) -- Fix requiring `tags` in server response (#1627 Thanks @evelyn-bi!) -- Add 10 minute timeout to HTTPClient (#1626) -- Fix save script without `-IncludeXml` (#1609, #1614 Thanks @o-l-a-v!) -- PAT token fix to translate into HttpClient 'Basic Authorization'(#1599 Thanks @gerryleys!) -- Fix incorrect request url when installing from ADO (#1597 Thanks @antonyoni!) -- Improved exception handling (#1569) -- Ensure that .NET methods are not called in order to enable use in Constrained Language Mode (#1564) -- PSResourceGet packaging update - -## [1.0.4.1](https://github.com/PowerShell/PSResourceGet/compare/v1.0.4...v1.0.4.1) - 2024-04-05 - -- PSResourceGet packaging update - -## [1.0.4](https://github.com/PowerShell/PSResourceGet/compare/v1.0.3...v1.0.4) - 2024-04-05 - -### Patch - -- Dependency package updates - -## 1.0.3 - -### Bug Fixes -- Bug fix for null package version in `Install-PSResource` - -## 1.0.2 - -### Bug Fixes - -- Bug fix for `Update-PSResource` not updating from correct repository (#1549) -- Bug fix for creating temp home directory on Unix (#1544) -- Bug fix for creating `InstalledScriptInfos` directory when it does not exist (#1542) -- Bug fix for `Update-ModuleManifest` throwing null pointer exception (#1538) -- Bug fix for `name` property not populating in `PSResourceInfo` object when using `Find-PSResource` with JFrog Artifactory (#1535) -- Bug fix for incorrect configuration of requests to JFrog Artifactory v2 endpoints (#1533 Thanks @sean-r-williams!) -- Bug fix for determining JFrog Artifactory repositories (#1532 Thanks @sean-r-williams!) -- Bug fix for v2 server repositories incorrectly adding script endpoint (1526) -- Bug fixes for null references (#1525) -- Typo fixes in message prompts in `Install-PSResource` (#1510 Thanks @NextGData!) -- Bug fix to add `NormalizedVersion` property to `AdditionalMetadata` only when it exists (#1503 Thanks @sean-r-williams!) -- Bug fix to verify whether `Uri` is a UNC path and set respective `ApiVersion` (#1479 Thanks @kborowinski!) - -## 1.0.1 - -### Bug Fixes - -- Bugfix to update Unix local user installation paths to be compatible with .NET 7 and .NET 8 (#1464) -- Bugfix for Import-PSGetRepository in Windows PowerShell (#1460) -- Bugfix for `Test-PSScriptFileInfo`` to be less sensitive to whitespace (#1457) -- Bugfix to overwrite rels/rels directory on net472 when extracting nupkg to directory (#1456) -- Bugfix to add pipeline by property name support for Name and Repository properties for Find-PSResource (#1451 Thanks @ThomasNieto!) - -## 1.0.0 - -### New Features -- Add `ApiVersion` parameter for `Register-PSResourceRepository` (#1431) - -### Bug Fixes -- Automatically set the ApiVersion to v2 for repositories imported from PowerShellGet (#1430) -- Bug fix ADO v2 feed installation failures (#1429) -- Bug fix Artifactory v2 endpoint failures (#1428) -- Bug fix Artifactory v3 endpoint failures (#1427) -- Bug fix `-RequiredResource` silent failures (#1426) -- Bug fix for v2 repository returning extra packages for `-Tag` based search with `-Prerelease` (#1405) - See change log (CHANGELOG) at https://github.com/PowerShell/PSResourceGet '@ } diff --git a/src/code/ContainerRegistryServerAPICalls.cs b/src/code/ContainerRegistryServerAPICalls.cs index 2c39f536d..9c17c0db0 100644 --- a/src/code/ContainerRegistryServerAPICalls.cs +++ b/src/code/ContainerRegistryServerAPICalls.cs @@ -54,6 +54,8 @@ internal class ContainerRegistryServerAPICalls : ServerApiCall const string containerRegistryRepositoryListTemplate = "https://{0}/v2/_catalog"; // 0 - registry + private string _cachedContainterRegistryToken = null; + #endregion #region Constructor @@ -68,6 +70,8 @@ public ContainerRegistryServerAPICalls(PSRepositoryInfo repository, PSCmdlet cmd Credentials = networkCredential }; + _cachedContainterRegistryToken = null; + _sessionClient = new HttpClient(handler); _sessionClient.Timeout = TimeSpan.FromMinutes(10); _sessionClient.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", userAgentString); @@ -328,7 +332,7 @@ private Stream InstallVersion( return null; } - string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, isPushOperation: false, out errRecord); if (errRecord != null) { return null; @@ -376,7 +380,7 @@ private Stream InstallVersion( /// If no credential provided at registration then, check if the ACR endpoint can be accessed without a token. If not, try using Azure.Identity to get the az access token, then ACR refresh token and then ACR access token. /// Note: Access token can be empty if the repository is unauthenticated /// - internal string GetContainerRegistryAccessToken(bool needCatalogAccess, out ErrorRecord errRecord) + internal string GetContainerRegistryAccessToken(bool needCatalogAccess, bool isPushOperation, out ErrorRecord errRecord) { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::GetContainerRegistryAccessToken()"); string accessToken = string.Empty; @@ -384,6 +388,12 @@ internal string GetContainerRegistryAccessToken(bool needCatalogAccess, out Erro string tenantID = string.Empty; errRecord = null; + if (!string.IsNullOrEmpty(_cachedContainterRegistryToken)) + { + _cmdletPassedIn.WriteVerbose("Using cached container registry access token."); + return _cachedContainterRegistryToken; + } + var repositoryCredentialInfo = Repository.CredentialInfo; if (repositoryCredentialInfo != null) { @@ -398,7 +408,10 @@ internal string GetContainerRegistryAccessToken(bool needCatalogAccess, out Erro } else { - bool isRepositoryUnauthenticated = IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), needCatalogAccess, out errRecord, out accessToken); + // A container registry repository is determined to be unauthenticated if it allows anonymous pull access. However, push operations always require authentication. + bool isRepositoryUnauthenticated = isPushOperation ? false : IsContainerRegistryUnauthenticated(Repository.Uri.ToString(), needCatalogAccess, out errRecord, out accessToken); + _cmdletPassedIn.WriteInformation($"Value of isRepositoryUnauthenticated: {isRepositoryUnauthenticated}", new string[] { "PSRGContainerRegistryUnauthenticatedCheck" }); + _cmdletPassedIn.WriteDebug($"Is repository unauthenticated: {isRepositoryUnauthenticated}"); if (errRecord != null) @@ -445,6 +458,9 @@ internal string GetContainerRegistryAccessToken(bool needCatalogAccess, out Erro return null; } + _cmdletPassedIn.WriteVerbose("Container registry access token retrieved."); + _cachedContainterRegistryToken = containerRegistryAccessToken; + return containerRegistryAccessToken; } @@ -739,7 +755,7 @@ internal Hashtable GetContainerRegistryMetadata(string packageName, string exact { using (JsonDocument metadataJSONDoc = JsonDocument.Parse(serverPkgInfo.Metadata)) { - string pkgVersionString = String.Empty; + string pkgVersionString = String.Empty; JsonElement rootDom = metadataJSONDoc.RootElement; if (rootDom.TryGetProperty("ModuleVersion", out JsonElement pkgVersionElement)) @@ -1014,6 +1030,7 @@ internal JObject GetHttpResponseJObjectUsingContentHeaders(string url, HttpMetho return null; } + // codeql[cs/sensitive-data-transmission] This is expected PSResourceGet behavior to create the content of the request which is only transmitted to the server, not the user. This information is also not exposed back to the user via error or verbose messaging. request.Content = new StringContent(content); request.Content.Headers.Clear(); if (contentHeaders != null) @@ -1316,7 +1333,7 @@ internal bool PushNupkgContainerRegistry( // Get access token (includes refresh tokens) _cmdletPassedIn.WriteVerbose($"Get access token for container registry server."); - var containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); + var containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, isPushOperation: true, out errRecord); if (errRecord != null) { return false; @@ -1781,7 +1798,7 @@ private Hashtable[] FindPackagesWithVersionHelper(string packageName, VersionTyp string packageNameLowercase = packageName.ToLower(); string packageNameForFind = PrependMARPrefix(packageNameLowercase); - string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: false, isPushOperation: false,out errRecord); if (errRecord != null) { return emptyHashResponses; @@ -1893,7 +1910,7 @@ private FindResults FindPackages(string packageName, bool includePrerelease, out { _cmdletPassedIn.WriteDebug("In ContainerRegistryServerAPICalls::FindPackages()"); errRecord = null; - string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: true, out errRecord); + string containerRegistryAccessToken = GetContainerRegistryAccessToken(needCatalogAccess: true, isPushOperation: false, out errRecord); if (errRecord != null) { return emptyResponseResults; diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index d8287c689..388be9090 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -1171,6 +1171,70 @@ internal IEnumerable FindDependencyPackages( } } } + else if(dep.VersionRange.MaxVersion != null && dep.VersionRange.MinVersion != null && dep.VersionRange.MaxVersion.OriginalVersion.Equals(dep.VersionRange.MinVersion.OriginalVersion)) + { + string depPkgVersion = dep.VersionRange.MaxVersion.OriginalVersion; + FindResults responses = currentServer.FindVersion(dep.Name, version: dep.VersionRange.MaxVersion.ToNormalizedString(), _type, out ErrorRecord errRecord); + if (errRecord != null) + { + if (errRecord.Exception is ResourceNotFoundException) + { + _cmdletPassedIn.WriteVerbose(errRecord.Exception.Message); + } + else + { + _cmdletPassedIn.WriteError(errRecord); + } + yield return null; + continue; + } + + PSResourceResult currentResult = currentResponseUtil.ConvertToPSResourceResult(responses).FirstOrDefault(); + if (currentResult == null) + { + // This scenario may occur when the package version requested is unlisted. + _cmdletPassedIn.WriteError(new ErrorRecord( + new ResourceNotFoundException($"Dependency package with name '{dep.Name}' and version '{depPkgVersion}' could not be found in repository '{repository.Name}'"), + "DependencyPackageNotFound", + ErrorCategory.ObjectNotFound, + this)); + yield return null; + continue; + } + + if (currentResult.exception != null && !currentResult.exception.Message.Equals(string.Empty)) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new ResourceNotFoundException($"Dependency package with name '{dep.Name}' and version '{depPkgVersion}' could not be found in repository '{repository.Name}'", currentResult.exception), + "DependencyPackageNotFound", + ErrorCategory.ObjectNotFound, + this)); + yield return null; + continue; + } + + depPkg = currentResult.returnedObject; + + if (!_packagesFound.ContainsKey(depPkg.Name)) + { + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + { + yield return depRes; + } + } + else + { + List pkgVersions = _packagesFound[depPkg.Name] as List; + // _packagesFound has depPkg.name in it, but the version is not the same + if (!pkgVersions.Contains(FormatPkgVersionString(depPkg))) + { + foreach (PSResourceInfo depRes in FindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository)) + { + yield return depRes; + } + } + } + } else { FindResults responses = currentServer.FindVersionGlobbing(dep.Name, dep.VersionRange, includePrerelease: true, ResourceType.None, getOnlyLatest: true, out ErrorRecord errRecord); diff --git a/src/code/FindPSResource.cs b/src/code/FindPSResource.cs index 2709073e7..a0ef9f496 100644 --- a/src/code/FindPSResource.cs +++ b/src/code/FindPSResource.cs @@ -234,6 +234,12 @@ private void ProcessResourceNameParameterSet() return; } + + if (versionRange.MinVersion != null && versionRange.MaxVersion != null && versionRange.MinVersion.OriginalVersion.Equals(versionRange.MaxVersion.OriginalVersion)) + { + nugetVersion = versionRange.MaxVersion; + versionType = VersionType.SpecificVersion; + } } else { diff --git a/src/code/GetInstalledPSResource.cs b/src/code/GetInstalledPSResource.cs index 90cc9f20d..9dfe1a670 100644 --- a/src/code/GetInstalledPSResource.cs +++ b/src/code/GetInstalledPSResource.cs @@ -15,7 +15,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets /// Returns a single resource or multiple resource. /// [Cmdlet(VerbsCommon.Get, "InstalledPSResource")] - [Alias("Get-PSResource")] + [Alias("Get-PSResource", "gres")] [OutputType(typeof(PSResourceInfo))] public sealed class GetInstalledPSResourceCommand : PSCmdlet { diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 83900e413..0616cf040 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -342,7 +342,7 @@ private List ProcessRepositories( allPkgsInstalled.AddRange(installedPkgs); } - if (!_cmdletPassedIn.MyInvocation.BoundParameters.ContainsKey("WhatIf") && _pkgNamesToInstall.Count > 0) + if ((!_cmdletPassedIn.MyInvocation.BoundParameters.ContainsKey("WhatIf") || (SwitchParameter)_cmdletPassedIn.MyInvocation.BoundParameters["WhatIf"] == false) && _pkgNamesToInstall.Count > 0) { string repositoryWording = repositoryNamesToSearch.Count > 1 ? "registered repositories" : "repository"; _cmdletPassedIn.WriteError(new ErrorRecord( @@ -587,6 +587,16 @@ private List InstallPackages( } } + string depPkgNameVersion = $"{depPkg.Name}{depPkg.Version.ToString()}"; + if (_packagesOnMachine.Contains(depPkgNameVersion) && !depPkg.IsPrerelease) + { + // if a dependency package is already installed, do not install it again. + // to determine if the package version is already installed, _packagesOnMachine is used but it only contains name, version info, not version with prerelease info + // if the dependency package is found to be prerelease, it is safer to install it (and worse case it reinstalls) + _cmdletPassedIn.WriteVerbose($"Dependency '{depPkg.Name}' with version '{depPkg.Version}' is already installed."); + continue; + } + packagesHash = BeginPackageInstall( searchVersionType: VersionType.SpecificVersion, specificVersion: depVersion, @@ -614,7 +624,7 @@ private List InstallPackages( } // If -WhatIf is passed in, early out. - if (_cmdletPassedIn.MyInvocation.BoundParameters.ContainsKey("WhatIf")) + if (_cmdletPassedIn.MyInvocation.BoundParameters.ContainsKey("WhatIf") && (SwitchParameter)_cmdletPassedIn.MyInvocation.BoundParameters["WhatIf"] == true) { return pkgsSuccessfullyInstalled; } diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 83943eef9..feca62d50 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -487,9 +487,9 @@ private void RequiredResourceHelper(Hashtable reqResourceHash) } } - if (pkgParams.Scope == ScopeType.AllUsers) + if (pkgParams.Scope.HasValue && pkgParams.Scope.Value == ScopeType.AllUsers) { - _pathsToInstallPkg = Utils.GetAllInstallationPaths(this, pkgParams.Scope); + _pathsToInstallPkg = Utils.GetAllInstallationPaths(this, pkgParams.Scope.Value); } pkgVersion = pkgInstallInfo["version"] == null ? String.Empty : pkgInstallInfo["version"].ToString(); @@ -565,6 +565,16 @@ private void ProcessInstallHelper(string[] pkgNames, string pkgVersion, bool pkg this)); } + // When reqResourceParams is provided (via -RequiredResource), use its properties + // instead of the cmdlet-level parameters. Only use the property if it was explicitly set (not null). + bool acceptLicense = reqResourceParams?.AcceptLicense ?? AcceptLicense; + bool quiet = reqResourceParams?.Quiet ?? Quiet; + bool reinstall = reqResourceParams?.Reinstall ?? Reinstall; + bool trustRepository = reqResourceParams?.TrustRepository ?? TrustRepository; + bool noClobber = reqResourceParams?.NoClobber ?? NoClobber; + bool skipDependencyCheck = reqResourceParams?.SkipDependencyCheck ?? SkipDependencyCheck; + ScopeType scope = reqResourceParams?.Scope ?? Scope; + IEnumerable installedPkgs = _installHelper.BeginInstallPackages( names: pkgNames, versionRange: versionRange, @@ -573,19 +583,19 @@ private void ProcessInstallHelper(string[] pkgNames, string pkgVersion, bool pkg versionString: Version, prerelease: pkgPrerelease, repository: pkgRepository, - acceptLicense: AcceptLicense, - quiet: Quiet, - reinstall: Reinstall, + acceptLicense: acceptLicense, + quiet: quiet, + reinstall: reinstall, force: false, - trustRepository: TrustRepository, - noClobber: NoClobber, + trustRepository: trustRepository, + noClobber: noClobber, asNupkg: false, includeXml: true, - skipDependencyCheck: SkipDependencyCheck, + skipDependencyCheck: skipDependencyCheck, authenticodeCheck: AuthenticodeCheck, savePkg: false, pathsToInstallPkg: _pathsToInstallPkg, - scope: Scope, + scope: scope, tmpPath: _tmpPath, pkgsInstalled: _packagesOnMachine); diff --git a/src/code/InstallPkgParams.cs b/src/code/InstallPkgParams.cs index 7a1b8e986..19cdd076b 100644 --- a/src/code/InstallPkgParams.cs +++ b/src/code/InstallPkgParams.cs @@ -11,15 +11,15 @@ public class InstallPkgParams public string Name { get; set; } public VersionRange Version { get; set; } public string Repository { get; set; } - public bool AcceptLicense { get; set; } + public bool? AcceptLicense { get; set; } public bool Prerelease { get; set; } - public ScopeType Scope { get; set; } - public bool Quiet { get; set; } - public bool Reinstall { get; set; } + public ScopeType? Scope { get; set; } + public bool? Quiet { get; set; } + public bool? Reinstall { get; set; } public bool Force { get; set; } - public bool TrustRepository { get; set; } - public bool NoClobber { get; set; } - public bool SkipDependencyCheck { get; set; } + public bool? TrustRepository { get; set; } + public bool? NoClobber { get; set; } + public bool? SkipDependencyCheck { get; set; } @@ -67,8 +67,10 @@ public void SetProperty(string propertyName, string propertyValue, out ErrorReco break; case "acceptlicense": - bool.TryParse(propertyValue, out bool acceptLicenseTmp); - AcceptLicense = acceptLicenseTmp; + if (!string.IsNullOrWhiteSpace(propertyValue) && bool.TryParse(propertyValue, out bool acceptLicenseTmp)) + { + AcceptLicense = acceptLicenseTmp; + } break; case "prerelease": @@ -82,28 +84,38 @@ public void SetProperty(string propertyName, string propertyValue, out ErrorReco break; case "quiet": - bool.TryParse(propertyValue, out bool quietTmp); - Quiet = quietTmp; + if (!string.IsNullOrWhiteSpace(propertyValue) && bool.TryParse(propertyValue, out bool quietTmp)) + { + Quiet = quietTmp; + } break; case "reinstall": - bool.TryParse(propertyValue, out bool reinstallTmp); - Reinstall = reinstallTmp; + if (!string.IsNullOrWhiteSpace(propertyValue) && bool.TryParse(propertyValue, out bool reinstallTmp)) + { + Reinstall = reinstallTmp; + } break; case "trustrepository": - bool.TryParse(propertyValue, out bool trustRepositoryTmp); - TrustRepository = trustRepositoryTmp; + if (!string.IsNullOrWhiteSpace(propertyValue) && bool.TryParse(propertyValue, out bool trustRepositoryTmp)) + { + TrustRepository = trustRepositoryTmp; + } break; case "noclobber": - bool.TryParse(propertyValue, out bool noClobberTmp); - NoClobber = noClobberTmp; + if (!string.IsNullOrWhiteSpace(propertyValue) && bool.TryParse(propertyValue, out bool noClobberTmp)) + { + NoClobber = noClobberTmp; + } break; case "skipdependencycheck": - bool.TryParse(propertyValue, out bool skipDependencyCheckTmp); - SkipDependencyCheck = skipDependencyCheckTmp; + if (!string.IsNullOrWhiteSpace(propertyValue) && bool.TryParse(propertyValue, out bool skipDependencyCheckTmp)) + { + SkipDependencyCheck = skipDependencyCheckTmp; + } break; default: diff --git a/src/code/Microsoft.PowerShell.PSResourceGet.csproj b/src/code/Microsoft.PowerShell.PSResourceGet.csproj index 715e420d4..daeaff8e1 100644 --- a/src/code/Microsoft.PowerShell.PSResourceGet.csproj +++ b/src/code/Microsoft.PowerShell.PSResourceGet.csproj @@ -22,9 +22,9 @@ - + - + diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 619c1da56..abdced37b 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -50,7 +50,6 @@ internal enum CallerCmdlet private string pathToModuleDirToPublish = string.Empty; private string pathToNupkgToPublish = string.Empty; private ResourceType resourceType = ResourceType.None; - private NetworkCredential _networkCredential; string userAgentString = UserAgentInfo.UserAgentString(); private bool _isNupkgPathSpecified = false; private Hashtable dependencies; @@ -381,7 +380,7 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe } // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. - _networkCredential = repository.SetNetworkCredentials(_networkCredential, _cmdletPassedIn); + var networkCredential = repository.SetNetworkCredentials(_networkCredential, _cmdletPassedIn); // Check if dependencies already exist within the repo if: // 1) the resource to publish has dependencies and @@ -389,7 +388,7 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe if (dependencies != null && !SkipDependenciesCheck) { // If error gets thrown, exit process record - if (!CheckDependenciesExist(dependencies, repository.Name)) + if (!CheckDependenciesExist(dependencies, repository.Name, networkCredential)) { return; } @@ -440,7 +439,7 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe if (repository.ApiVersion == PSRepositoryInfo.APIVersion.ContainerRegistry) { - ContainerRegistryServerAPICalls containerRegistryServer = new ContainerRegistryServerAPICalls(repository, _cmdletPassedIn, _networkCredential, userAgentString); + ContainerRegistryServerAPICalls containerRegistryServer = new ContainerRegistryServerAPICalls(repository, _cmdletPassedIn, networkCredential, userAgentString); if (_isNupkgPathSpecified) { @@ -475,7 +474,7 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe } // This call does not throw any exceptions, but it will write unsuccessful responses to the console - if (!PushNupkg(outputNupkgDir, repository.Name, repository.Uri.ToString(), out ErrorRecord pushNupkgError)) + if (!PushNupkg(outputNupkgDir, repository.Name, repository.Uri.ToString(), networkCredential, out ErrorRecord pushNupkgError)) { _cmdletPassedIn.WriteError(pushNupkgError); // exit out of processing @@ -605,7 +604,8 @@ private bool PackNupkg(string outputDir, string outputNupkgDir, string nuspecFil Path = nuspecFile, Exclude = System.Array.Empty(), Symbols = false, - Logger = NullLogger.Instance + Logger = NullLogger.Instance, + NoDefaultExcludes = true }, MSBuildProjectFactory.ProjectCreator, builder); @@ -647,7 +647,7 @@ private bool PackNupkg(string outputDir, string outputNupkgDir, string nuspecFil return true; } - private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, out ErrorRecord error) + private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, NetworkCredential networkCredential, out ErrorRecord error) { _cmdletPassedIn.WriteDebug("In PublishPSResource::PushNupkg()"); @@ -673,9 +673,9 @@ private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, o var success = false; var sourceProvider = new PackageSourceProvider(settings); - if (Credential != null || _networkCredential != null) + if (Credential != null || networkCredential != null) { - InjectCredentialsToSettings(settings, sourceProvider, publishLocation); + InjectCredentialsToSettings(settings, sourceProvider, publishLocation, networkCredential); } @@ -832,10 +832,10 @@ private bool PushNupkg(string outputNupkgDir, string repoName, string repoUri, o return success; } - private void InjectCredentialsToSettings(ISettings settings, IPackageSourceProvider sourceProvider, string source) + private void InjectCredentialsToSettings(ISettings settings, IPackageSourceProvider sourceProvider, string source, NetworkCredential networkCredential) { _cmdletPassedIn.WriteDebug("In PublishPSResource::InjectCredentialsToSettings()"); - if (Credential == null && _networkCredential == null) + if (Credential == null && networkCredential == null) { return; } @@ -850,7 +850,7 @@ private void InjectCredentialsToSettings(ISettings settings, IPackageSourceProvi } - var networkCred = Credential == null ? _networkCredential : Credential.GetNetworkCredential(); + var networkCred = Credential == null ? networkCredential : Credential.GetNetworkCredential(); string key; if (packageSource == null) @@ -1245,7 +1245,7 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) return dependenciesHash; } - private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName) + private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName, NetworkCredential networkCredential) { _cmdletPassedIn.WriteDebug("In PublishHelper::CheckDependenciesExist()"); @@ -1275,7 +1275,7 @@ private bool CheckDependenciesExist(Hashtable dependencies, string repositoryNam } // Search for and return the dependency if it's in the repository. - FindHelper findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn, _networkCredential); + FindHelper findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn, networkCredential); var repository = new[] { repositoryName }; // Note: we set prerelease argument for FindByResourceName() to true because if no version is specified we want latest version (including prerelease). diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 8d86face3..a4994df8a 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -91,6 +91,7 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters /// Specifies the Api version of the repository to be set. /// [Parameter(ParameterSetName = NameParameterSet)] + [ValidateSet("V2", "V3", "Local", "NugetServer", "ContainerRegistry")] public PSRepositoryInfo.APIVersion ApiVersion { get; set; } /// @@ -185,17 +186,20 @@ protected override void ProcessRecord() break; case PSGalleryParameterSet: - try - { - items.Add(PSGalleryParameterSetHelper(Priority, Trusted)); - } - catch (Exception e) + if (PSGallery) { - ThrowTerminatingError(new ErrorRecord( - new PSInvalidOperationException(e.Message), - "ErrorInPSGalleryParameterSet", - ErrorCategory.InvalidArgument, - this)); + try + { + items.Add(PSGalleryParameterSetHelper(Priority, Trusted)); + } + catch (Exception e) + { + ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException(e.Message), + "ErrorInPSGalleryParameterSet", + ErrorCategory.InvalidArgument, + this)); + } } break; diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index 39f129006..7cf4f9261 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -72,7 +72,7 @@ public static void CheckRepositoryStore() } catch (Exception e) { - throw new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Repository store may be corrupted, file reading failed with error: {0}.", e.Message)); + throw new PSInvalidOperationException(string.Format(CultureInfo.InvariantCulture, "Repository store may be corrupted, file reading failed with error: {0}. Try running 'Reset-PSResourceRepository' to reset the repository store.", e.Message)); } } @@ -845,6 +845,116 @@ public static List Read(string[] repoNames, out string[] error return reposToReturn.ToList(); } + /// + /// Reset the repository store by creating a new PSRepositories.xml file with PSGallery registered. + /// This creates a temporary new file first, and only replaces the old file if creation succeeds. + /// If creation fails, the old file is restored. + /// Returns: PSRepositoryInfo for the PSGallery repository + /// + public static PSRepositoryInfo Reset(out string errorMsg) + { + errorMsg = string.Empty; + string tempFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".xml"); + string backupFilePath = string.Empty; + + try + { + // Ensure the repository directory exists + if (!Directory.Exists(RepositoryPath)) + { + Directory.CreateDirectory(RepositoryPath); + } + + // Create new repository XML in a temporary location + XDocument newRepoXML = new XDocument( + new XElement("configuration")); + newRepoXML.Save(tempFilePath); + + // Validate that the temporary file can be loaded + try + { + LoadXDocument(tempFilePath); + } + catch (Exception loadEx) + { + // Clean up temp file on validation failure + if (File.Exists(tempFilePath)) + { + try + { + File.Delete(tempFilePath); + } + catch (Exception cleanupEx) + { + errorMsg = string.Format(CultureInfo.InvariantCulture, "Failed to validate newly created repository store file with error: {0}. Additionally, cleanup of temporary file failed with error: {1}", loadEx.Message, cleanupEx.Message); + return null; + } + } + errorMsg = string.Format(CultureInfo.InvariantCulture, "Failed to validate newly created repository store file with error: {0}.", loadEx.Message); + return null; + } + + // Back up the existing file if it exists + if (File.Exists(FullRepositoryPath)) + { + backupFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + "_backup.xml"); + Utils.MoveFiles(FullRepositoryPath, backupFilePath, overwrite: true); + } + + // Move the temporary file to the actual location + Utils.MoveFiles(tempFilePath, FullRepositoryPath, overwrite: true); + + // Add PSGallery to the newly created store + Uri psGalleryUri = new Uri(PSGalleryRepoUri); + PSRepositoryInfo psGalleryRepo = Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); + + // Clean up backup file on success + if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath)) + { + File.Delete(backupFilePath); + } + + return psGalleryRepo; + } + catch (Exception e) + { + // Restore the backup file if it exists + if (!string.IsNullOrEmpty(backupFilePath) && File.Exists(backupFilePath)) + { + try + { + if (File.Exists(FullRepositoryPath)) + { + File.Delete(FullRepositoryPath); + } + Utils.MoveFiles(backupFilePath, FullRepositoryPath, overwrite: true); + } + catch (Exception restoreEx) + { + errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}. An attempt to restore the old repository store also failed with error: {1}", e.Message, restoreEx.Message); + return null; + } + } + + // Clean up temporary file + if (File.Exists(tempFilePath)) + { + try + { + File.Delete(tempFilePath); + } + catch (Exception cleanupEx) + { + errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}. Additionally, cleanup of temporary file failed with error: {1}", e.Message, cleanupEx.Message); + return null; + } + } + + errorMsg = string.Format(CultureInfo.InvariantCulture, "Repository store reset failed with error: {0}.", e.Message); + return null; + } + } + #endregion #region Private methods diff --git a/src/code/ResetPSResourceRepository.cs b/src/code/ResetPSResourceRepository.cs new file mode 100644 index 000000000..2c217aab3 --- /dev/null +++ b/src/code/ResetPSResourceRepository.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PSResourceGet.UtilClasses; +using System; +using System.Management.Automation; + +namespace Microsoft.PowerShell.PSResourceGet.Cmdlets +{ + /// + /// The Reset-PSResourceRepository cmdlet resets the repository store by creating a new PSRepositories.xml file. + /// This is useful when the repository store becomes corrupted. + /// It will create a new repository store with only the PSGallery repository registered. + /// + [Cmdlet(VerbsCommon.Reset, + "PSResourceRepository", + SupportsShouldProcess = true, + ConfirmImpact = ConfirmImpact.High)] + [OutputType(typeof(PSRepositoryInfo))] + public sealed class ResetPSResourceRepository : PSCmdlet + { + #region Parameters + + /// + /// When specified, displays the PSGallery repository that was registered after reset + /// + [Parameter] + public SwitchParameter PassThru { get; set; } + + #endregion + + #region Methods + + protected override void ProcessRecord() + { + string repositoryStorePath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "PSResourceGet", + "PSResourceRepository.xml"); + + WriteVerbose($"Resetting repository store at: {repositoryStorePath}"); + + if (!ShouldProcess(repositoryStorePath, "Reset repository store and create new PSRepositories.xml file with PSGallery registered")) + { + return; + } + + PSRepositoryInfo psGalleryRepo = RepositorySettings.Reset(out string errorMsg); + + if (!string.IsNullOrEmpty(errorMsg)) + { + WriteError(new ErrorRecord( + new PSInvalidOperationException(errorMsg), + "ErrorResettingRepositoryStore", + ErrorCategory.InvalidOperation, + this)); + return; + } + + WriteVerbose("Repository store reset successfully. PSGallery has been registered."); + + if (PassThru) + { + WriteObject(psGalleryRepo); + } + } + + #endregion + } +} diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index 26d481fce..de5a70808 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -17,6 +17,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets /// It returns nothing. /// [Cmdlet(VerbsData.Save, "PSResource", DefaultParameterSetName = "IncludeXmlParameterSet", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)] + [Alias("svres")] public sealed class SavePSResource : PSCmdlet { #region Members diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index d46c5ba48..8dc17fe21 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -88,6 +88,7 @@ public SwitchParameter Trusted /// Specifies the Api version of the repository to be set. /// [Parameter(ParameterSetName = NameParameterSet)] + [ValidateSet("V2", "V3", "Local", "NugetServer", "ContainerRegistry")] public PSRepositoryInfo.APIVersion ApiVersion { get; set; } /// diff --git a/src/code/UninstallPSResource.cs b/src/code/UninstallPSResource.cs index 4cd0a4d19..3183900fd 100644 --- a/src/code/UninstallPSResource.cs +++ b/src/code/UninstallPSResource.cs @@ -17,6 +17,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets /// Uninstall-PSResource uninstalls a package found in a module or script installation path. /// [Cmdlet(VerbsLifecycle.Uninstall, "PSResource", DefaultParameterSetName = NameParameterSet, SupportsShouldProcess = true)] + [Alias("usres")] public sealed class UninstallPSResource : PSCmdlet { #region Parameters @@ -181,6 +182,8 @@ private bool UninstallPkgHelper(out List errRecords) WriteDebug("In UninstallPSResource::UninstallPkgHelper"); var successfullyUninstalled = false; GetHelper getHelper = new GetHelper(this); + + HashSet requestedPackageNames = new HashSet(Name, StringComparer.InvariantCultureIgnoreCase); List dirsToDelete = getHelper.FilterPkgPathsByName(Name, _pathsToSearch); int totalDirs = dirsToDelete.Count; errRecords = new List(); @@ -256,6 +259,20 @@ private bool UninstallPkgHelper(out List errRecords) return successfullyUninstalled; } + + requestedPackageNames.Remove(pkgName); + } + + // the package requested for uninstallation was found by name, but not satisfied by version criteria (i.e version didn't exist or match prerelease criteria) so write error + if (requestedPackageNames.Count > 0) + { + string[] pkgsFailedToUninstall = requestedPackageNames.ToArray(); + string prereleaseMessage = Prerelease ? "prerelease " : String.Empty; + string versionMessage = Version != null ? $"matching '{Version} '" : String.Empty; + + string warningMessage = $"Cannot uninstall {prereleaseMessage}version(s) {versionMessage}of resource '{String.Join(", ", pkgsFailedToUninstall)}' because it does not exist."; + + WriteWarning(warningMessage); } return successfullyUninstalled; diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 86e2cf1ae..6cfc4fbbe 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -25,6 +25,7 @@ public sealed class UpdatePSResource : PSCmdlet { #region Members private List _pathsToInstallPkg; + private HashSet _packagesOnMachine; private CancellationTokenSource _cancellationTokenSource; private FindHelper _findHelper; private InstallHelper _installHelper; @@ -157,6 +158,8 @@ protected override void BeginProcessing() RepositorySettings.CheckRepositoryStore(); _pathsToInstallPkg = Utils.GetAllInstallationPaths(this, Scope); + List pathsToSearch = Utils.GetAllResourcePaths(this, Scope); + _packagesOnMachine = Utils.GetInstalledPackages(pathsToSearch, this); _cancellationTokenSource = new CancellationTokenSource(); var networkCred = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; @@ -216,7 +219,7 @@ protected override void ProcessRecord() pathsToInstallPkg: _pathsToInstallPkg, scope: Scope, tmpPath: _tmpPath, - pkgsInstalled: new HashSet(StringComparer.InvariantCultureIgnoreCase)); + pkgsInstalled: _packagesOnMachine); if (PassThru) { diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 61b9e6b04..26d3ab25e 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -189,13 +189,18 @@ public static string[] ProcessNameWildcards( if (name.Contains("?") || name.Contains("[")) { - errorMsgsList.Add(String.Format("-Name with wildcards '?' and '[' are not supported for this cmdlet so Name entry: {0} will be discarded.", name)); + errorMsgsList.Add(String.Format("-Name with wildcards '?' and '[' are not supported for this cmdlet so Name entry: '{0}' will be discarded.", name)); continue; } isContainWildcard = true; namesWithSupportedWildcards.Add(name); } + else if(name.StartsWith("/") || name.StartsWith("\\")) + { + errorMsgsList.Add(String.Format("-Name starting with path separator '/' or '\\' is not supported for this cmdlet so Name entry: '{0}' will be discarded.", name)); + continue; + } else { namesWithSupportedWildcards.Add(name); @@ -662,6 +667,7 @@ public static string GetAzAccessToken(PSCmdlet cmdletPassedIn) ExcludeInteractiveBrowserCredential = false }; + // codeql[cs/security/identity/default-azure-credential-use] DefaultAzureCredential is not being used to create a credential in a production environment (i.e hosted server). It is created locally for a PSResourceGet command invocation, intended to be short-lived, and supports multiple authentication mechanisms which cannot be predicted and isolated for the invocation beforehand. var dCred = new DefaultAzureCredential(credOptions); var tokenRequestContext = new TokenRequestContext(new string[] { "https://management.azure.com/.default" }); @@ -1375,7 +1381,7 @@ private static bool TryReadPSDataFile( public static bool ValidateModuleManifest(string moduleManifestPath, out string errorMsg) { errorMsg = string.Empty; - using (System.Management.Automation.PowerShell pwsh = System.Management.Automation.PowerShell.Create()) + using (System.Management.Automation.PowerShell pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) { // use PowerShell cmdlet Test-ModuleManifest // TODO: Test-ModuleManifest will throw an error if RequiredModules specifies a module that does not exist @@ -1400,32 +1406,32 @@ public static bool ValidateModuleManifest(string moduleManifestPath, out string } } - if (pwsh.HadErrors) + // Validate the result object directly + if (results.Any()) { - if (results.Any()) + PSModuleInfo psModuleInfoObj = results[0].BaseObject as PSModuleInfo; + if (string.IsNullOrWhiteSpace(psModuleInfoObj.Author)) { - PSModuleInfo psModuleInfoObj = results[0].BaseObject as PSModuleInfo; - if (string.IsNullOrWhiteSpace(psModuleInfoObj.Author)) - { - errorMsg = "No author was provided in the module manifest. The module manifest must specify a version, author and description. Run 'Test-ModuleManifest' to validate the file."; - } - else if (string.IsNullOrWhiteSpace(psModuleInfoObj.Description)) - { - errorMsg = "No description was provided in the module manifest. The module manifest must specify a version, author and description. Run 'Test-ModuleManifest' to validate the file."; - } - else if (psModuleInfoObj.Version == null) - { - errorMsg = "No version or an incorrectly formatted version was provided in the module manifest. The module manifest must specify a version, author and description. Run 'Test-ModuleManifest' to validate the file."; - } + errorMsg = "No author was provided in the module manifest. The module manifest must specify a version, author and description. Run 'Test-ModuleManifest' to validate the file."; + return false; } - - if (string.IsNullOrEmpty(errorMsg)) + else if (string.IsNullOrWhiteSpace(psModuleInfoObj.Description)) { - // Surface any inner error messages - var innerErrorMsg = (pwsh.Streams.Error.Count > 0) ? pwsh.Streams.Error[0].ToString() : string.Empty; - errorMsg = $"Module manifest file validation failed with error: {innerErrorMsg}. Run 'Test-ModuleManifest' to validate the module manifest."; + errorMsg = "No description was provided in the module manifest. The module manifest must specify a version, author and description. Run 'Test-ModuleManifest' to validate the file."; + return false; } - + else if (psModuleInfoObj.Version == null) + { + errorMsg = "No version or an incorrectly formatted version was provided in the module manifest. The module manifest must specify a version, author and description. Run 'Test-ModuleManifest' to validate the file."; + return false; + } + } + + // Check for any errors from Test-ModuleManifest + if (pwsh.HadErrors) + { + var innerErrorMsg = (pwsh.Streams.Error.Count > 0) ? pwsh.Streams.Error[0].ToString() : string.Empty; + errorMsg = $"Module manifest file validation failed with error: {innerErrorMsg}. Run 'Test-ModuleManifest' to validate the module manifest."; return false; } } @@ -1658,6 +1664,16 @@ public static void DeleteDirectoryWithRestore(string dirPath) } } + private static void SetAttributesHelper(DirectoryInfo directory) + { + foreach (var subDirectory in directory.GetDirectories()) + { + subDirectory.Attributes = FileAttributes.Normal; + SetAttributesHelper(subDirectory); + } + + directory.Attributes = FileAttributes.Normal; + } /// /// Deletes a directory and its contents /// This is a workaround for .NET Directory.Delete(), which can fail with WindowsPowerShell @@ -1672,13 +1688,17 @@ public static void DeleteDirectory(string dirPath) } // Remove read only file attributes first - foreach (var dirFilePath in Directory.GetFiles(dirPath,"*",SearchOption.AllDirectories)) + foreach (var dirFilePath in Directory.GetFiles(dirPath, "*", SearchOption.AllDirectories)) { if (File.GetAttributes(dirFilePath).HasFlag(FileAttributes.ReadOnly)) { File.SetAttributes(dirFilePath, File.GetAttributes(dirFilePath) & ~FileAttributes.ReadOnly); } } + + DirectoryInfo rootDir = new DirectoryInfo(dirPath); + SetAttributesHelper(rootDir); + // Delete directory recursive, try multiple times before throwing ( #1662 ) int maxAttempts = 5; int msDelay = 5; @@ -1686,7 +1706,7 @@ public static void DeleteDirectory(string dirPath) { try { - Directory.Delete(dirPath,true); + Directory.Delete(dirPath, true); return; } catch (Exception ex) @@ -1695,6 +1715,17 @@ public static void DeleteDirectory(string dirPath) { Thread.Sleep(msDelay); } + else if (ex is System.IO.IOException) + { + string psVersion = System.Management.Automation.Runspaces.Runspace.DefaultRunspace.Version.ToString(); + if (ex.Message.Contains("The directory is not empty") && psVersion.StartsWith("5")) + { + // there is a known bug with WindowsPowerShell and OneDrive based module paths, where .NET Directory.Delete() will throw a 'The directory is not empty.' error. + throw new Exception(string.Format("Cannot uninstall module with OneDrive based path on Windows PowerShell due to .NET issue. Try installing and uninstalling using PowerShell 7+ if using OneDrive."), ex); + } + + throw new Exception(string.Format("Access denied to path while deleting path {0}", dirPath), ex); + } else { throw; @@ -2160,6 +2191,7 @@ internal static bool CheckAuthenticodeSignature( // Because authenticode and catalog verifications are only applicable on Windows, we allow all packages by default to be installed on unix systems. if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + cmdletPassedIn.WriteWarning("Authenticode check cannot be performed on Linux or MacOS."); return true; } diff --git a/src/code/V2ServerAPICalls.cs b/src/code/V2ServerAPICalls.cs index 94d0b3a0b..ec3551f79 100644 --- a/src/code/V2ServerAPICalls.cs +++ b/src/code/V2ServerAPICalls.cs @@ -638,6 +638,17 @@ public override FindResults FindVersion(string packageName, string version, Reso // Quotations around package name and version do not matter, same metadata gets returned. // We need to explicitly add 'Id eq ' whenever $filter is used, otherwise arbitrary results are returned. + // version passed in must be a valid NuGet version + if (!NuGetVersion.TryParse(version, out NuGetVersion nugetVersion)) + { + errRecord = new ErrorRecord( + new ArgumentException($"Version '{version}' cannot be converted to a valid NuGet version."), + "InvalidVersionFormat", + ErrorCategory.InvalidArgument, + this); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v2FindResponseType); + } + var queryBuilder = new NuGetV2QueryBuilder(new Dictionary{ { "$inlinecount", "allpages" }, { "id", $"'{packageName}'" }, @@ -650,7 +661,8 @@ public override FindResults FindVersion(string packageName, string version, Reso filterBuilder.AddCriterion($"Id eq '{packageName}'"); } - filterBuilder.AddCriterion($"NormalizedVersion eq '{version}'"); + // a NormalizedVersion is required for the query filter + filterBuilder.AddCriterion($"NormalizedVersion eq '{nugetVersion.ToNormalizedString()}'"); if (type != ResourceType.None) { filterBuilder.AddCriterion(GetTypeFilterForRequest(type)); } diff --git a/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 index 139e68ab0..911471f3b 100644 --- a/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 @@ -446,6 +446,17 @@ Describe 'Test HTTP Find-PSResource for V2 Server Protocol' -tags 'CI' { $res.Name | Should -Contain 'test_unlisted' $res.Name | Should -Contain 'test_notunlisted' } + + It "Find resource that takes a dependency on package with specific version" { + $moduleName = 'test-nugetversion-parent' + $version = '4.0.0' + $depPkgName = 'test-nugetversion' + + $res = Find-PSResource -Name $moduleName -Version $version -Repository $PSGalleryName -IncludeDependencies + $res.Count | Should -Be 2 + $res.Name | Should -Contain $moduleName + $res.Name | Should -Contain $depPkgName + } } Describe 'Test HTTP Find-PSResource for V2 Server Protocol' -tags 'ManualValidationOnly' { diff --git a/test/GetInstalledPSResource/GetInstalledPSResource.Tests.ps1 b/test/GetInstalledPSResource/GetInstalledPSResource.Tests.ps1 index 5ddbf816a..21887bea3 100644 --- a/test/GetInstalledPSResource/GetInstalledPSResource.Tests.ps1 +++ b/test/GetInstalledPSResource/GetInstalledPSResource.Tests.ps1 @@ -153,6 +153,10 @@ Describe 'Test Get-InstalledPSResource for Module' -tags 'CI' { (Get-Alias Get-PSResource).Definition | Should -BeExactly 'Get-InstalledPSResource' } + It "Get definition for alias 'gres'" { + (Get-Alias gres).Definition | Should -BeExactly 'Get-InstalledPSResource' + } + It "Should not throw on ErrorAction ignore when no subdirectories are found" { { Get-InstalledPSResource -Path $TestEmptyDirectoryPath -ErrorAction 'Ignore' } | Should -Not -Throw } diff --git a/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 index 46d4c54a6..eea4dae94 100644 --- a/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceRepositorySearching.Tests.ps1 @@ -137,4 +137,12 @@ Describe 'Test Install-PSResource for searching and looping through repositories $err | Should -HaveCount 0 $warningVar | Should -Not -BeNullOrEmpty } + + It "install resources from repository should respect WhatIf value of false" { + # Package "test_module" exists in the following repositories (in this order): localRepo, PSGallery, NuGetGallery + $res = Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -SkipDependencyCheck -PassThru -WhatIf:$false + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $testModuleName + $res.Repository | Should -Be $PSGalleryName + } } diff --git a/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 index 6023355b7..ee35c3396 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 @@ -32,7 +32,7 @@ Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'CI' { AfterEach { Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", ` "TestFindModule", "ClobberTestModule1", "ClobberTestModule2", "PackageManagement", "TestTestScript", ` - "TestModuleWithDependency", "TestModuleWithPrereleaseDep", "PrereleaseModule" -SkipDependencyCheck -ErrorAction SilentlyContinue + "TestModuleWithDependency", "TestModuleWithPrereleaseDep", "PrereleaseModule", "test-nugetversion-parent", "test-nugetversion", "test-pkg-normalized-dependency" -SkipDependencyCheck -ErrorAction SilentlyContinue } AfterAll { @@ -429,6 +429,19 @@ Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'CI' { (Get-InstalledPSResource -Name 'TestModule99').'Prerelease' | Should -Be 'beta2' } + It "Install module using -RequiredResource with TrustRepository in hashtable" { + # This test verifies that TrustRepository specified in -RequiredResource hashtable is respected + Install-PSResource -RequiredResource @{ + 'TestModule99' = @{ + 'repository' = 'PSGallery' + 'trustrepository' = 'true' + } + } + $res = Get-InstalledPSResource -Name 'TestModule99' + $res.Name | Should -Be 'TestModule99' + $res.Version | Should -Be '0.0.93' + } + It "Install modules using -RequiredResource with JSON string" { $rrJSON = "{ 'test_module': { @@ -554,6 +567,14 @@ Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'CI' { $err[0].FullyQualifiedErrorId | Should -BeExactly "GetAuthenticodeSignatureError,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" } + # Test that AuthenticodeCheck parameter displays warning on non-Windows + It "Install with AuthenticodeCheck on non-Windows should display warning" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -AuthenticodeCheck -WarningVariable warn -WarningAction SilentlyContinue + $warn[0] | Should -Match "Authenticode check cannot be performed on Linux or MacOS" + $res = Get-InstalledPSResource $testModuleName + $res.Name | Should -Be $testModuleName + } + # Unix test for installing scripts It "Install script resource - Unix only" -Skip:(Get-IsWindows) { # previously installing pester on Unix was throwing an error due to how the environment PATH variable was being gotten. @@ -595,6 +616,37 @@ Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'CI' { $res | Should -Not -BeNullOrEmpty $res.Version | Should -Be $version } + + It "Install resource that takes a dependency on package with specific version" { + $moduleName = 'test-nugetversion-parent' + $version = '4.0.0' + $depPkgName = 'test-nugetversion' + $depPkgVer = '5.0.1' + + Install-PSResource -Name $moduleName -Version $version -Repository $PSGalleryName -TrustRepository + $res = Get-InstalledPSResource $moduleName + $res.Name | Should -Be $moduleName + $res.Version | Should -Be $version + $depRes = Get-InstalledPSResource $depPkgName + $depRes.Name | Should -Be $depPkgName + $depRes.Version | Should -Be $depPkgVer + } + + It "Install resource that takes a dependency on package with specific version with differing normalized and semver versions" { + $moduleName = 'test-pkg-normalized-dependency' + $version = '3.9.2' + $depPkgName1 = "PowerShellGet" + $depPkgName2 = "PackageManagement" + + Install-PSResource -Name $moduleName -Prerelease -Repository $PSGalleryName -TrustRepository + $res = Get-InstalledPSResource $moduleName + $res.Name | Should -Be $moduleName + $res.Version | Should -Be $version + + $depRes = Get-InstalledPSResource $depPkgName1, $depPkgName2 + $depRes.Name | Should -Contain $depPkgName1 + $depRes.Name | Should -Contain $depPkgName2 + } } Describe 'Test Install-PSResource for V2 Server scenarios' -tags 'ManualValidationOnly' { diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 index e18b9272d..835e0c0c6 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -310,6 +310,19 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { $res3.Version | Should -Be '0.0.93' } + It 'Install module using -RequiredResource with TrustRepository in hashtable' { + # This test verifies that TrustRepository specified in -RequiredResource hashtable is respected + Install-PSResource -RequiredResource @{ + 'TestModule99' = @{ + 'repository' = $NuGetGalleryName + 'trustrepository' = 'true' + } + } + $res = Get-InstalledPSResource -Name 'TestModule99' + $res.Name | Should -Be 'TestModule99' + $res.Version | Should -Be '0.0.93' + } + It 'Install modules using -RequiredResource with JSON string' { $rrJSON = "{ 'test_module': { diff --git a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 index 75205069e..5dfb6e3d9 100644 --- a/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/CompressPSResource.Tests.ps1 @@ -396,6 +396,36 @@ Describe "Test Compress-PSResource" -tags 'CI' { } } + It "Compress-PSResource includes .gitkeep files (empty and non-empty)" { + $version = "1.0.0" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + + # Create 'hidden' directory with .gitkeep files + $hiddenDir = Join-Path -Path $script:PublishModuleBase -ChildPath "hidden" + New-Item -Path $hiddenDir -ItemType Directory -Force + + # Create empty .gitkeep file in 'hidden' directory + $hiddenGitkeep = Join-Path -Path $hiddenDir -ChildPath ".gitkeep" + New-Item -Path $hiddenGitkeep -ItemType File -Force + + Compress-PSResource -Path $script:PublishModuleBase -DestinationPath $script:repositoryPath + + # Extract and verify files are included + $nupkgPath = Join-Path -Path $script:repositoryPath -ChildPath "$script:PublishModuleName.$version.nupkg" + $zipPath = Join-Path -Path $script:repositoryPath -ChildPath "$script:PublishModuleName.$version.zip" + Rename-Item -Path $nupkgPath -NewName $zipPath + $unzippedPath = Join-Path -Path $TestDrive -ChildPath "$script:PublishModuleName-gitkeep-test" + New-Item $unzippedPath -ItemType directory -Force + Expand-Archive -Path $zipPath -DestinationPath $unzippedPath + + # Verify both .gitkeep files exist + $extractedHiddenkeep = Join-Path -Path $unzippedPath -ChildPath "hidden" | Join-Path -ChildPath ".gitkeep" + + Test-Path -Path $extractedHiddenkeep | Should -Be $True + + $null = Remove-Item $unzippedPath -Force -Recurse + } + <# Test for Signing the nupkg. Signing doesn't work It "Compressed Module is able to be signed with a certificate" { $version = "1.0.0" diff --git a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 index e97f64407..ed0adf1b8 100644 --- a/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResource.Tests.ps1 @@ -131,6 +131,21 @@ Describe "Test Publish-PSResource" -tags 'CI' { } } + It "Publish a module with valid Author field without -SkipModuleManifestValidate" { + # This test verifies that the fix for runspace deserialization issue works correctly. + # Previously, PSModuleInfo.Author would return empty string when called via PowerShell.Create(), + # causing false positive "No author was provided" errors. + $version = "1.0.0" + $author = "TestAuthor" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" -Author $author + + # This should succeed without needing -SkipModuleManifestValidate + Publish-PSResource -Path $script:PublishModuleBase -Repository $testRepository2 + + $expectedPath = Join-Path -Path $script:repositoryPath2 -ChildPath "$script:PublishModuleName.$version.nupkg" + (Get-ChildItem $script:repositoryPath2).FullName | Should -Be $expectedPath + } + It "Publish a module with -Path to the highest priority repo" { $version = "1.0.0" New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" diff --git a/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 b/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 index 9714a627c..bd59d69d0 100644 --- a/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/PublishPSResourceTests/PublishPSResourceContainerRegistryServer.Tests.ps1 @@ -346,6 +346,25 @@ Describe "Test Publish-PSResource" -tags 'CI' { $results[0].Version | Should -Be $correctVersion } + It "Publish a package should always require authentication" { + $version = "15.0.0" + New-ModuleManifest -Path (Join-Path -Path $script:PublishModuleBase -ChildPath "$script:PublishModuleName.psd1") -ModuleVersion $version -Description "$script:PublishModuleName module" + + Publish-PSResource -Path $script:PublishModuleBase -Repository $ACRRepoName -InformationVariable RegistryUnauthenticated + + $results = Find-PSResource -Name $script:PublishModuleName -Repository $ACRRepoName + $results | Should -Not -BeNullOrEmpty + $results[0].Name | Should -Be $script:PublishModuleName + $results[0].Version | Should -Be $version + + if ($usingAzAuth) + { + $RegistryUnauthenticated | Should -Not -BeNullOrEmpty + $RegistryUnauthenticated[0].Tags | Should -Be "PSRGContainerRegistryUnauthenticatedCheck" + $RegistryUnauthenticated[0].MessageData | Should -Be "Value of isRepositoryUnauthenticated: False" + } + } + It "Publish a script"{ $scriptVersion = "1.0.0" $params = @{ diff --git a/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 index 93df34b22..f9c350356 100644 --- a/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/RegisterPSResourceRepository.Tests.ps1 @@ -85,6 +85,12 @@ Describe "Test Register-PSResourceRepository" -tags 'CI' { $res.Priority | Should -Be 50 } + It "register repository with PSGallery switch parameter value of false (PSGalleryParameterSet)" { + Unregister-PSResourceRepository -Name $PSGalleryName + $res = Register-PSResourceRepository -PSGallery:$false -PassThru + $res | Should -BeNullOrEmpty + } + It "register repository with PSGallery, Trusted parameters (PSGalleryParameterSet)" { Unregister-PSResourceRepository -Name $PSGalleryName $res = Register-PSResourceRepository -PSGallery -Trusted -PassThru @@ -404,6 +410,14 @@ Describe "Test Register-PSResourceRepository" -tags 'CI' { $res.ApiVersion | Should -Be 'v2' } + It "should throw error when trying to register repository with ApiVersion unknown" { + {Register-PSResourceRepository -Name $TestRepoName1 -Uri $tmpDir1Path -ApiVersion "unknown" -ErrorAction Stop} | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.PSResourceGet.Cmdlets.RegisterPSResourceRepository" + + # Verify the repository was not created + $repo = Get-PSResourceRepository $TestRepoName1 -ErrorAction SilentlyContinue + $repo | Should -BeNullOrEmpty + } + It "should register container registry repository with correct ApiVersion" { $ContainerRegistryName = "ACRRepo" $ContainerRegistryUri = "https://psresourcegettest.azurecr.io/" diff --git a/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 new file mode 100644 index 000000000..f49570229 --- /dev/null +++ b/test/ResourceRepositoryTests/ResetPSResourceRepository.Tests.ps1 @@ -0,0 +1,149 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$modPath = "$psscriptroot/../PSGetTestUtils.psm1" +Write-Verbose -Verbose -Message "PSGetTestUtils path: $modPath" +Import-Module $modPath -Force -Verbose + +Describe "Test Reset-PSResourceRepository" -tags 'CI' { + BeforeEach { + $PSGalleryName = Get-PSGalleryName + $PSGalleryUri = Get-PSGalleryLocation + Get-NewPSResourceRepositoryFile + } + + AfterEach { + Get-RevertPSResourceRepositoryFile + } + + It "Reset repository store without PassThru parameter" { + # Arrange: Add a test repository + $TestRepoName = "testRepository" + $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null + Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath + + # Verify repository was added + $repos = Get-PSResourceRepository + $repos.Count | Should -BeGreaterThan 1 + + # Act: Reset repository store + Reset-PSResourceRepository -Confirm:$false + + # Assert: Only PSGallery should exist + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + $repos.Name | Should -Be $PSGalleryName + $repos.Uri | Should -Be $PSGalleryUri + } + + It "Reset repository store with PassThru parameter returns PSGallery" { + # Arrange: Add a test repository + $TestRepoName = "testRepository" + $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null + Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath + + # Act: Reset repository store with PassThru + $result = Reset-PSResourceRepository -Confirm:$false -PassThru + + # Assert: Result should be PSGallery repository info + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $PSGalleryName + $result.Uri | Should -Be $PSGalleryUri + $result.Trusted | Should -Be $false + $result.Priority | Should -Be 50 + + # Verify only PSGallery exists + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + } + + It "Reset repository store should support -WhatIf" { + # Arrange: Add a test repository + $TestRepoName = "testRepository" + $tmpDirPath = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + New-Item -ItemType Directory -Path $tmpDirPath -Force | Out-Null + Register-PSResourceRepository -Name $TestRepoName -Uri $tmpDirPath + + # Capture repository count before WhatIf + $reposBefore = Get-PSResourceRepository + $countBefore = $reposBefore.Count + + # Act: Run with WhatIf + Reset-PSResourceRepository -WhatIf + + # Assert: Repositories should not have changed + $reposAfter = Get-PSResourceRepository + $reposAfter.Count | Should -Be $countBefore + } + + It "Reset repository store when corrupted should succeed" { + # Arrange: Corrupt the repository file + $powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet" + $repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml" + + # Write invalid XML to corrupt the file + "This is not valid XML" | Set-Content -Path $repoFilePath -Force + + # Act: Reset the repository store + $result = Reset-PSResourceRepository -Confirm:$false -PassThru + + # Assert: Should successfully reset and return PSGallery + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $PSGalleryName + + # Verify we can now read repositories + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + $repos.Name | Should -Be $PSGalleryName + } + + It "Reset repository store when file doesn't exist should succeed" { + # Arrange: Delete the repository file + $powerShellGetPath = Join-Path -Path ([Environment]::GetFolderPath([System.Environment+SpecialFolder]::LocalApplicationData)) -ChildPath "PSResourceGet" + $repoFilePath = Join-Path -Path $powerShellGetPath -ChildPath "PSResourceRepository.xml" + + if (Test-Path -Path $repoFilePath) { + Remove-Item -Path $repoFilePath -Force + } + + # Act: Reset the repository store + $result = Reset-PSResourceRepository -Confirm:$false -PassThru + + # Assert: Should successfully reset and return PSGallery + $result | Should -Not -BeNullOrEmpty + $result.Name | Should -Be $PSGalleryName + + # Verify PSGallery is registered + $repos = Get-PSResourceRepository + $repos.Count | Should -Be 1 + $repos.Name | Should -Be $PSGalleryName + } + + It "Reset repository store with multiple repositories should only keep PSGallery" { + # Arrange: Register multiple repositories + $tmpDir1Path = Join-Path -Path $TestDrive -ChildPath "tmpDir1" + $tmpDir2Path = Join-Path -Path $TestDrive -ChildPath "tmpDir2" + $tmpDir3Path = Join-Path -Path $TestDrive -ChildPath "tmpDir3" + New-Item -ItemType Directory -Path $tmpDir1Path -Force | Out-Null + New-Item -ItemType Directory -Path $tmpDir2Path -Force | Out-Null + New-Item -ItemType Directory -Path $tmpDir3Path -Force | Out-Null + + Register-PSResourceRepository -Name "testRepo1" -Uri $tmpDir1Path + Register-PSResourceRepository -Name "testRepo2" -Uri $tmpDir2Path + Register-PSResourceRepository -Name "testRepo3" -Uri $tmpDir3Path + + # Verify multiple repositories exist + $reposBefore = Get-PSResourceRepository + $reposBefore.Count | Should -BeGreaterThan 1 + + # Act: Reset repository store + Reset-PSResourceRepository -Confirm:$false + + # Assert: Only PSGallery should remain + $reposAfter = Get-PSResourceRepository + $reposAfter.Count | Should -Be 1 + $reposAfter.Name | Should -Be $PSGalleryName + } +} diff --git a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 index fa9120dfe..3a4d4f10c 100644 --- a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 @@ -345,15 +345,16 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { $repo.Priority | Should -Be 25 } - It "should not change ApiVersion of repository if -ApiVersion parameter was not used" { + It "should throw error when trying to set ApiVersion to unknown" { Register-PSResourceRepository -Name $TestRepoName1 -Uri $tmpDir1Path $repo = Get-PSResourceRepository $TestRepoName1 $repoApiVersion = $repo.ApiVersion $repoApiVersion | Should -Be "local" - Set-PSResourceRepository -Name $TestRepoName1 -ApiVersion "unknown" -ErrorVariable err -ErrorAction SilentlyContinue + {Set-PSResourceRepository -Name $TestRepoName1 -ApiVersion "unknown" -ErrorAction Stop} | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" + + # Verify the repository ApiVersion was not changed $repo = Get-PSResourceRepository $TestRepoName1 - $repo.ApiVersion | Should -Be "unknown" - $err.Count | Should -Be 0 + $repo.ApiVersion | Should -Be "local" } } diff --git a/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 b/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 index 5b090e739..b25a6c53b 100644 --- a/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceLocal.Tests.ps1 @@ -205,4 +205,8 @@ Describe 'Test Save-PSResource for local repositories' -tags 'CI' { $res.Name | Should -Be $moduleName $res.Version | Should -Be "1.0.0" } + + It "Get definition for alias 'svres'" { + (Get-Alias svres).Definition | Should -BeExactly 'Save-PSResource' + } } diff --git a/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 b/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 index 609c73e3d..4b0269d82 100644 --- a/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceV2.Tests.ps1 @@ -208,4 +208,10 @@ Describe 'Test HTTP Save-PSResource for V2 Server Protocol' -tags 'CI' { $pkg.Name | Should -Be $testModuleNameWithLicense $pkg.Version | Should -Be "2.0" } + + It "Not save module that has path separator in name" { + Save-PSResource -Name "/$testModuleName" -Repository $PSGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly 'ErrorFilteringNamesForUnsupportedWildcards,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource' + } } diff --git a/test/SavePSResourceTests/SavePSResourceV3.Tests.ps1 b/test/SavePSResourceTests/SavePSResourceV3.Tests.ps1 index 037ed1674..f4118d265 100644 --- a/test/SavePSResourceTests/SavePSResourceV3.Tests.ps1 +++ b/test/SavePSResourceTests/SavePSResourceV3.Tests.ps1 @@ -159,4 +159,10 @@ Describe 'Test HTTP Save-PSResource for V3 Server Protocol' -tags 'CI' { $res = Save-PSResource 'TestModuleWithDependencyE' -Repository $NuGetGalleryName -TrustRepository -PassThru $res.Length | Should -Be 4 } + + It "Not save module that has path separator in name" { + Save-PSResource -Name "/$testModuleName" -Repository $NuGetGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly 'ErrorFilteringNamesForUnsupportedWildcards,Microsoft.PowerShell.PSResourceGet.Cmdlets.SavePSResource' + } } diff --git a/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 b/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 index 0a1fa10fc..45b9310a1 100644 --- a/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 +++ b/test/UninstallPSResourceTests/UninstallPSResource.Tests.ps1 @@ -110,6 +110,32 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { $pkgs.Version | Should -Not -Contain "1.0.0" } + It "Do not uninstall existing module when requested version does not exist and write warning instead" { + Uninstall-PSResource -Name $testModuleName -Version "9.9.9" -SkipDependencyCheck -WarningVariable warn -WarningAction SilentlyContinue + + # Module should still be present since no prerelease versions were found + $res = Get-InstalledPSResource -Name $testModuleName + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $testModuleName + + # Warning should have been written + $warn.Count | Should -Be 1 + $warn[0] | Should -Match "Cannot uninstall version" + } + + It "Do not uninstall existing module when requested version range does not exist and write warning instead" { + Uninstall-PSResource -Name $testModuleName -Version "[9.9.9, 10.0.0]" -SkipDependencyCheck -WarningVariable warn -WarningAction SilentlyContinue + + # Module should still be present since no prerelease versions were found + $res = Get-InstalledPSResource -Name $testModuleName + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $testModuleName + + # Warning should have been written + $warn.Count | Should -Be 1 + $warn[0] | Should -Match "Cannot uninstall version" + } + $testCases = @{Version="[1.0.0.0]"; ExpectedVersion="1.0.0.0"; Reason="validate version, exact match"}, @{Version="1.0.0.0"; ExpectedVersion="1.0.0.0"; Reason="validate version, exact match without bracket syntax"}, @{Version="[1.0.0.0, 5.0.0.0]"; ExpectedVersion="5.0.0.0"; Reason="validate version, exact range inclusive"}, @@ -235,6 +261,35 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { $stableVersionPkgs.Count | Should -Be 2 } + It "Write warning when using -Prerelease flag with only stable versions installed" { + # $testModuleName (test_module2) only has stable versions installed + $pkg = Get-InstalledPSResource $testModuleName + $pkg | Should -Not -BeNullOrEmpty + + # Try to uninstall with -Prerelease flag, should show warning + Uninstall-PSResource -Name $testModuleName -Prerelease -SkipDependencyCheck -WarningVariable warn -WarningAction SilentlyContinue + + # Module should still be present since no prerelease versions were found + $res = Get-InstalledPSResource -Name $testModuleName + $res | Should -Not -BeNullOrEmpty + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be $pkg.Version + + # Warning should have been written + $warn.Count | Should -Be 1 + $warn[0] | Should -Match "Cannot uninstall prerelease version" + } + + It "Write warning when multiple modules are requested to be uninstalled but one does not exist" { + Uninstall-PSResource $testModuleName, "nonExistantModule" -SkipDependencyCheck -WarningVariable warn -WarningAction SilentlyContinue + $res = Get-InstalledPSResource -Name $testModuleName + $res | Should -BeNullOrEmpty + + # Warning should have been written + $warn.Count | Should -Be 1 + $warn[0] | Should -Match "Cannot uninstall version" + } + It "Uninstall module using -WhatIf, should not uninstall the module" { Start-Transcript .\testUninstallWhatIf.txt Uninstall-PSResource -Name $testModuleName -WhatIf -SkipDependencyCheck @@ -340,4 +395,8 @@ Describe 'Test Uninstall-PSResource for Modules' -tags 'CI' { $pkg.Name | Should -Be $testModuleName $pkg.Path.ToString().Contains("Documents") | Should -Be $true } + + It "Get definition for alias 'usres'" { + (Get-Alias usres).Definition | Should -BeExactly 'Uninstall-PSResource' + } }