diff --git a/doBuild.ps1 b/doBuild.ps1 index 8ce91adc1..c4c0983a8 100644 --- a/doBuild.ps1 +++ b/doBuild.ps1 @@ -91,6 +91,7 @@ function DoBuild 'NuGet.Repositories' 'NuGet.Versioning' 'Newtonsoft.Json' + 'System.Text.Json' ) $buildSuccess = $true diff --git a/src/PSGet.Format.ps1xml b/src/PSGet.Format.ps1xml index 5a7f14e81..9c9eb7b40 100644 --- a/src/PSGet.Format.ps1xml +++ b/src/PSGet.Format.ps1xml @@ -27,6 +27,28 @@ + + PSCommandResourceInfo + + Microsoft.PowerShell.PowerShellGet.UtilClasses.PSCommandResourceInfo + + + + + + + + + + + Names + $_.ParentResource.Name + $_.ParentResource.Version + + + + + PSIncludedResourceInfoTable diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 97457d74d..0e0c3a8f2 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -15,6 +15,7 @@ using System.Management.Automation; using System.Net; using System.Net.Http; +using System.Runtime.ExceptionServices; using System.Threading; namespace Microsoft.PowerShell.PowerShellGet.Cmdlets @@ -29,20 +30,20 @@ internal class FindHelper private CancellationToken _cancellationToken; private readonly PSCmdlet _cmdletPassedIn; private List _pkgsLeftToFind; + private List _tagsLeftToFind; private ResourceType _type; private string _version; + private VersionRange _versionRange; + private NuGetVersion _nugetVersion; + private VersionType _versionType; private SwitchParameter _prerelease = false; - private PSCredential _credential; private string[] _tag; private SwitchParameter _includeDependencies = false; private readonly string _psGalleryRepoName = "PSGallery"; private readonly string _psGalleryScriptsRepoName = "PSGalleryScripts"; - private readonly string _psGalleryUri = "https://www.powershellgallery.com/api/v2"; private readonly string _poshTestGalleryRepoName = "PoshTestGallery"; - private readonly string _poshTestGalleryScriptsRepoName = "PoshTestGalleryScripts"; - private readonly string _poshTestGalleryUri = "https://www.poshtestgallery.com/api/v2"; - private bool _isADOFeedRepository; private bool _repositoryNameContainsWildcard; + private NetworkCredential _networkCredential; // NuGet's SearchAsync() API takes a top parameter of 6000, but testing shows for PSGallery // usually a max of around 5990 is returned while more are left to retrieve in a second SearchAsync() call @@ -56,46 +57,50 @@ internal class FindHelper private FindHelper() { } - public FindHelper(CancellationToken cancellationToken, PSCmdlet cmdletPassedIn) + public FindHelper(CancellationToken cancellationToken, PSCmdlet cmdletPassedIn, NetworkCredential networkCredential) { _cancellationToken = cancellationToken; _cmdletPassedIn = cmdletPassedIn; + _networkCredential = networkCredential; } #endregion - #region Public methods + #region Public Methods - public List FindByResourceName( + public IEnumerable FindByResourceName( string[] name, ResourceType type, + VersionRange versionRange, + NuGetVersion nugetVersion, + VersionType versionType, string version, SwitchParameter prerelease, string[] tag, string[] repository, - PSCredential credential, SwitchParameter includeDependencies) { _type = type; _version = version; _prerelease = prerelease; - _tag = tag; - _credential = credential; + _tag = tag ?? Utils.EmptyStrArray; _includeDependencies = includeDependencies; - - List foundPackages = new List(); + _versionRange = versionRange; + _nugetVersion = nugetVersion; + _versionType = versionType; if (name.Length == 0) { - return foundPackages; + yield break; } - _pkgsLeftToFind = name.ToList(); + _pkgsLeftToFind = new List(name); + _tagsLeftToFind = tag == null ? new List() : new List(tag); // Error out if repository array of names to be searched contains wildcards. if (repository != null) { - repository = Utils.ProcessNameWildcards(repository, out string[] errorMsgs, out _repositoryNameContainsWildcard); + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _repositoryNameContainsWildcard); foreach (string error in errorMsgs) { _cmdletPassedIn.WriteError(new ErrorRecord( @@ -111,6 +116,15 @@ public List FindByResourceName( try { repositoriesToSearch = RepositorySettings.Read(repository, out string[] errorList); + if (repository != null && repositoriesToSearch.Count == 0) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + "RepositoryNameIsNotResolved", + ErrorCategory.InvalidArgument, + this)); + } + foreach (string error in errorList) { _cmdletPassedIn.WriteError(new ErrorRecord( @@ -128,177 +142,716 @@ public List FindByResourceName( ErrorCategory.InvalidArgument, this)); - return foundPackages; + yield break; } - // Loop through repositoriesToSearch and if PSGallery or PoshTestGallery add its Scripts endpoint repo - // to list with same priority as PSGallery repo. - // This special casing is done to handle PSGallery and PoshTestGallery having 2 endpoints currently for different resources. - for (int i = 0; i < repositoriesToSearch.Count; i++) + for (int i = 0; i < repositoriesToSearch.Count && _pkgsLeftToFind.Any(); i++) { - if (String.Equals(repositoriesToSearch[i].Uri.AbsoluteUri, _psGalleryUri, StringComparison.InvariantCultureIgnoreCase)) + if (repositoriesToSearch[i].ApiVersion == PSRepositoryInfo.APIVersion.local) { - // special case: for PowerShellGallery, Module and Script resources have different endpoints so separate repositories have to be registered - // with those endpoints in order for the NuGet APIs to search across both in the case where name includes '*' - - // detect if Script repository needs to be added and/or Module repository needs to be skipped - Uri psGalleryScriptsUri = new Uri("http://www.powershellgallery.com/api/v2/items/psscript/"); - PSRepositoryInfo psGalleryScripts = new PSRepositoryInfo(_psGalleryScriptsRepoName, psGalleryScriptsUri, repositoriesToSearch[i].Priority, trusted: false, credentialInfo: null); - if (_type == ResourceType.None) - { - _cmdletPassedIn.WriteVerbose("Null Type provided, so add PSGalleryScripts repository"); - repositoriesToSearch.Insert(i + 1, psGalleryScripts); - } - else if (_type != ResourceType.None && _type == ResourceType.Script) + foreach (PSResourceInfo currentPkg in SearchFromLocalRepository(repositoriesToSearch[i])) { - _cmdletPassedIn.WriteVerbose("Type Script provided, so add PSGalleryScripts and remove PSGallery (Modules only) from search consideration"); - repositoriesToSearch.Insert(i + 1, psGalleryScripts); - repositoriesToSearch.RemoveAt(i); // remove PSGallery + yield return currentPkg; } } - else if (String.Equals(repositoriesToSearch[i].Uri.AbsoluteUri, _poshTestGalleryUri, StringComparison.InvariantCultureIgnoreCase)) + else { - // special case: for PoshTestGallery, Module and Script resources have different endpoints so separate repositories have to be registered - // with those endpoints in order for the NuGet APIs to search across both in the case where name includes '*' + PSRepositoryInfo currentRepository = repositoriesToSearch[i]; - // detect if Script repository needs to be added and/or Module repository needs to be skipped - Uri poshTestGalleryScriptsUri = new Uri("https://www.poshtestgallery.com/api/v2/items/psscript/"); - PSRepositoryInfo poshTestGalleryScripts = new PSRepositoryInfo(_poshTestGalleryScriptsRepoName, poshTestGalleryScriptsUri, repositoriesToSearch[i].Priority, trusted: false, credentialInfo: null); - if (_type == ResourceType.None) + // Explicitly passed in Credential takes precedence over repository CredentialInfo. + if (_networkCredential == null && currentRepository.CredentialInfo != null) { - _cmdletPassedIn.WriteVerbose("Null Type provided, so add PoshTestGalleryScripts repository"); - repositoriesToSearch.Insert(i + 1, poshTestGalleryScripts); + PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( + currentRepository.Name, + currentRepository.CredentialInfo, + _cmdletPassedIn); + + var username = repoCredential.UserName; + var password = repoCredential.Password; + + _networkCredential = new NetworkCredential(username, password); + + _cmdletPassedIn.WriteVerbose("credential successfully read from vault and set for repository: " + currentRepository.Name); } - else if (_type != ResourceType.None && _type == ResourceType.Script) + + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _networkCredential); + ResponseUtil currentResponseUtil = ResponseUtilFactory.GetResponseUtil(currentRepository); + + _cmdletPassedIn.WriteVerbose(string.Format("Searching in repository {0}", repositoriesToSearch[i].Name)); + + + foreach (PSResourceInfo currentPkg in SearchByNames(currentServer, currentResponseUtil, currentRepository)) { - _cmdletPassedIn.WriteVerbose("Type Script provided, so add PoshTestGalleryScripts and remove PoshTestGallery (Modules only) from search consideration"); - repositoriesToSearch.Insert(i + 1, poshTestGalleryScripts); - repositoriesToSearch.RemoveAt(i); // remove PoshTestGallery + yield return currentPkg; } } + } + } + public IEnumerable FindCommandOrDscResource( + bool isSearchingForCommands, + SwitchParameter prerelease, + string[] tag, + string[] repository) + { + _prerelease = prerelease; + + List cmdsLeftToFind = new List(tag); + + if (tag.Length == 0) + { + yield break; } - for (int i = 0; i < repositoriesToSearch.Count && _pkgsLeftToFind.Any(); i++) + // Error out if repository array of names to be searched contains wildcards. + if (repository != null) { - _cmdletPassedIn.WriteVerbose(string.Format("Searching in repository {0}", repositoriesToSearch[i].Name)); - foreach (var pkg in SearchFromRepository( - repositoryName: repositoriesToSearch[i].Name, - repositoryUri: repositoriesToSearch[i].Uri, - repositoryCredentialInfo: repositoriesToSearch[i].CredentialInfo)) + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _repositoryNameContainsWildcard); + + if (string.Equals(repository[0], "*")) { - foundPackages.Add(pkg); + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSArgumentException ("-Repository parameter does not support entry '*' with -CommandName and -DSCResourceName parameters."), + "RepositoryDoesNotSupportWildcardEntryWithCmdOrDSCName", + ErrorCategory.InvalidArgument, + this)); + } + + foreach (string error in errorMsgs) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new PSInvalidOperationException(error), + "ErrorFilteringNamesForUnsupportedWildcards", + ErrorCategory.InvalidArgument, + this)); } } - return foundPackages; - } + // Get repositories to search. + List repositoriesToSearch; + try + { + repositoriesToSearch = RepositorySettings.Read(repository, out string[] errorList); + if (repository != null && repositoriesToSearch.Count == 0) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + "RepositoryNameIsNotResolved", + ErrorCategory.InvalidArgument, + this)); + } - #endregion + foreach (string error in errorList) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new PSInvalidOperationException(error), + "ErrorGettingSpecifiedRepo", + ErrorCategory.InvalidOperation, + this)); + } + } + catch (Exception e) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException(e.Message), + "ErrorLoadingRepositoryStoreFile", + ErrorCategory.InvalidArgument, + this)); + + yield break; + } - #region Private methods + for (int i = 0; i < repositoriesToSearch.Count && cmdsLeftToFind.Any(); i++) + { + PSRepositoryInfo currentRepository = repositoriesToSearch[i]; + + if (repositoriesToSearch[i].ApiVersion == PSRepositoryInfo.APIVersion.local) + { + _pkgsLeftToFind = new List() { "*" }; - private IEnumerable SearchFromRepository( - string repositoryName, - Uri repositoryUri, - PSCredentialInfo repositoryCredentialInfo) + var potentialMatches = SearchFromLocalRepository(repositoriesToSearch[i]); + foreach (string cmdOrDsc in tag) + { + foreach (var package in potentialMatches) + { + // this check ensures DSC names provided as a Command name won't get returned mistakenly + // -CommandName "command1", "dsc1" <- (will not return or add DSC name) + if ((isSearchingForCommands && package.Includes.Command.Contains(cmdOrDsc, StringComparer.OrdinalIgnoreCase)) || + (!isSearchingForCommands && package.Includes.DscResource.Contains(cmdOrDsc, StringComparer.OrdinalIgnoreCase))) + { + yield return new PSCommandResourceInfo(new string[] { cmdOrDsc }, package); + } + } + } + } + else + { + // Explicitly passed in Credential takes precedence over repository CredentialInfo. + if (_networkCredential == null && currentRepository.CredentialInfo != null) + { + PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( + currentRepository.Name, + currentRepository.CredentialInfo, + _cmdletPassedIn); + + var username = repoCredential.UserName; + var password = repoCredential.Password; + + _networkCredential = new NetworkCredential(username, password); + + _cmdletPassedIn.WriteVerbose("credential successfully read from vault and set for repository: " + currentRepository.Name); + } + + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _networkCredential); + ResponseUtil currentResponseUtil = ResponseUtilFactory.GetResponseUtil(currentRepository); + + _cmdletPassedIn.WriteVerbose(string.Format("Searching in repository {0}", repositoriesToSearch[i].Name)); + + foreach (string currentCmdOrDSC in tag) + { + string[] responses = currentServer.FindCommandOrDscResource(currentCmdOrDSC, _prerelease, isSearchingForCommands, out ExceptionDispatchInfo edi); + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindCommandOrDSCResourceFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + foreach (PSResourceResult currentResult in currentResponseUtil.ConvertToPSResourceResult(responses: responses)) + { + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(currentResult.errorMsg), "FindCmdOrDSCResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + cmdsLeftToFind.Remove(currentCmdOrDSC); + PSCommandResourceInfo currentCmdPkg = new PSCommandResourceInfo(new string[] { currentCmdOrDSC }, currentResult.returnedObject); + yield return currentCmdPkg; + } + } + } + } + } + + public IEnumerable FindTag( + ResourceType type, + SwitchParameter prerelease, + string[] tag, + string[] repository) { - PackageSearchResource resourceSearch; - PackageMetadataResource resourceMetadata; - SearchFilter filter; - SourceCacheContext context; + _type = type; + _prerelease = prerelease; + _tag = tag; - // File based Uri scheme. - if (repositoryUri.Scheme == Uri.UriSchemeFile) + _tagsLeftToFind = new List(tag); + + if (tag.Length == 0) { - FindLocalPackagesResourceV2 localResource = new FindLocalPackagesResourceV2(repositoryUri.ToString()); - resourceSearch = new LocalPackageSearchResource(localResource); - resourceMetadata = new LocalPackageMetadataResource(localResource); - filter = new SearchFilter(_prerelease); - context = new SourceCacheContext(); + yield break; + } - foreach(PSResourceInfo pkg in SearchAcrossNamesInRepository( - repositoryName: repositoryName, - pkgSearchResource: resourceSearch, - pkgMetadataResource: resourceMetadata, - searchFilter: filter, - sourceContext: context)) + if (repository != null) + { + repository = Utils.ProcessNameWildcards(repository, removeWildcardEntries:false, out string[] errorMsgs, out _repositoryNameContainsWildcard); + + if (string.Equals(repository[0], "*")) { - yield return pkg; + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSArgumentException ("-Repository parameter does not support entry '*' with -Tag parameter."), + "RepositoryDoesNotSupportWildcardEntryWithTag", + ErrorCategory.InvalidArgument, + this)); } + + foreach (string error in errorMsgs) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new PSInvalidOperationException(error), + "ErrorFilteringNamesForUnsupportedWildcards", + ErrorCategory.InvalidArgument, + this)); + } + } + + // Get repositories to search. + List repositoriesToSearch; + try + { + repositoriesToSearch = RepositorySettings.Read(repository, out string[] errorList); + if (repository != null && repositoriesToSearch.Count == 0) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSArgumentException ("Cannot resolve -Repository name. Run 'Get-PSResourceRepository' to view all registered repositories."), + "RepositoryNameIsNotResolved", + ErrorCategory.InvalidArgument, + this)); + } + + foreach (string error in errorList) + { + _cmdletPassedIn.WriteError(new ErrorRecord( + new PSInvalidOperationException(error), + "ErrorGettingSpecifiedRepo", + ErrorCategory.InvalidOperation, + this)); + } + } + catch (Exception e) + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException(e.Message), + "ErrorLoadingRepositoryStoreFile", + ErrorCategory.InvalidArgument, + this)); + yield break; } - // Check if ADOFeed- for which searching for Name with wildcard has a different logic flow. - if (repositoryUri.ToString().Contains("pkgs.")) + for (int i = 0; i < repositoriesToSearch.Count && _tagsLeftToFind.Any(); i++) { - _isADOFeedRepository = true; + PSRepositoryInfo currentRepository = repositoriesToSearch[i]; + + if (repositoriesToSearch[i].ApiVersion == PSRepositoryInfo.APIVersion.local) + { + _pkgsLeftToFind = new List() { "*" }; + _tag = tag; + + foreach (var package in SearchFromLocalRepository(repositoriesToSearch[i])) + { + yield return package; + } + } + else + { + // Explicitly passed in Credential takes precedence over repository CredentialInfo. + if (_networkCredential == null && currentRepository.CredentialInfo != null) + { + PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( + currentRepository.Name, + currentRepository.CredentialInfo, + _cmdletPassedIn); + + var username = repoCredential.UserName; + var password = repoCredential.Password; + + _networkCredential = new NetworkCredential(username, password); + + _cmdletPassedIn.WriteVerbose("credential successfully read from vault and set for repository: " + currentRepository.Name); + } + + + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _networkCredential); + ResponseUtil currentResponseUtil = ResponseUtilFactory.GetResponseUtil(currentRepository); + + if (_type != ResourceType.None && repositoriesToSearch[i].Name != "PSGallery") + { + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new PSInvalidOperationException("-Type parameter is only supported with the PowerShellGallery."), + "ErrorUsingTypeParameter", + ErrorCategory.InvalidOperation, + this)); + } + + foreach (string currentTag in _tag) + { + string[] responses = currentServer.FindTag(currentTag, _prerelease, type, out ExceptionDispatchInfo edi); + + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindTagFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + foreach (PSResourceResult currentResult in currentResponseUtil.ConvertToPSResourceResult(responses: responses)) + { + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + string errMsg = $"Tags: {String.Join(", ", _tag)} could not be found due to: {currentResult.errorMsg}"; + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(errMsg), "FindTagResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + yield return currentResult.returnedObject; + } + } + } } + } + + #endregion + + #region Private HTTP Client Search Methods - // HTTP, HTTPS, FTP Uri schemes (only other Uri schemes allowed by RepositorySettings.Read() API). - PackageSource source = new PackageSource(repositoryUri.ToString()); + private IEnumerable SearchByNames(ServerApiCall currentServer, ResponseUtil currentResponseUtil, PSRepositoryInfo repository) + { + ExceptionDispatchInfo edi = null; + List parentPkgs = new List(); + HashSet pkgsFound = new HashSet(StringComparer.OrdinalIgnoreCase); - // Explicitly passed in Credential takes precedence over repository CredentialInfo. - if (_credential != null) + foreach (string pkgName in _pkgsLeftToFind.ToArray()) { - string password = new NetworkCredential(string.Empty, _credential.Password).Password; - source.Credentials = PackageSourceCredential.FromUserInput(repositoryUri.ToString(), _credential.UserName, password, true, null); - _cmdletPassedIn.WriteVerbose("credential successfully set for repository: " + repositoryName); + if (_versionType == VersionType.NoVersion) + { + if (pkgName.Trim().Equals("*")) + { + // Example: Find-PSResource -Name "*" + string[] responses = currentServer.FindAll(_prerelease, _type, out edi); + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindAllFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + foreach (PSResourceResult currentResult in currentResponseUtil.ConvertToPSResourceResult(responses: responses)) + { + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + string errMsg = $"Package with search criteria: Name {pkgName} could not be found due to: {currentResult.errorMsg}."; + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(errMsg), "FindAllResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + PSResourceInfo foundPkg = currentResult.returnedObject; + parentPkgs.Add(foundPkg); + pkgsFound.Add(Utils.CreateHashSetKey(foundPkg.Name, foundPkg.Version.ToString())); + yield return foundPkg; + } + } + else if(pkgName.Contains("*")) + { + // Example: Find-PSResource -Name "Az*" + // Example: Find-PSResource -Name "Az*" -Tag "Storage" + string tagMsg = String.Empty; + string[] responses = Utils.EmptyStrArray; + if (_tag.Length == 0) + { + responses = currentServer.FindNameGlobbing(pkgName, _prerelease, _type, out edi); + } + else + { + responses = currentServer.FindNameGlobbingWithTag(pkgName, _tag, _prerelease, _type, out edi); + string tagsAsString = String.Join(", ", _tag); + tagMsg = $" and Tags {tagsAsString}"; + } + + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindNameGlobbingFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + foreach (PSResourceResult currentResult in currentResponseUtil.ConvertToPSResourceResult(responses: responses)) + { + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + string errMsg = $"Package with search criteria: Name {pkgName}{tagMsg} could not be found due to: {currentResult.errorMsg} originating at method: FindNameGlobbingResponseConversionFail()."; + _cmdletPassedIn.WriteWarning(errMsg); + continue; + } + + PSResourceInfo foundPkg = currentResult.returnedObject; + parentPkgs.Add(foundPkg); + pkgsFound.Add(Utils.CreateHashSetKey(foundPkg.Name, foundPkg.Version.ToString())); + yield return foundPkg; + } + } + else + { + // Example: Find-PSResource -Name "Az" + // Example: Find-PSResource -Name "Az" -Tag "Storage" + string tagMsg = String.Empty; + string response = String.Empty; + if (_tag.Length == 0) + { + response = currentServer.FindName(pkgName, _prerelease, _type, out edi); + } + else + { + response = currentServer.FindNameWithTag(pkgName, _tag, _prerelease, _type, out edi); + string tagsAsString = String.Join(", ", _tag); + tagMsg = $" and Tags {tagsAsString}"; + } + + string[] responses = new string[]{ response }; + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindNameFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + PSResourceResult currentResult = currentResponseUtil.ConvertToPSResourceResult(responses: responses).First(); + + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + string errMsg = $"Package with search criteria: Name {pkgName}{tagMsg} could not be found due to: {currentResult.errorMsg}."; + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(errMsg), "FindNameResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + PSResourceInfo foundPkg = currentResult.returnedObject; + parentPkgs.Add(foundPkg); + pkgsFound.Add(Utils.CreateHashSetKey(foundPkg.Name, foundPkg.Version.ToString())); + yield return foundPkg; + } + } + else if (_versionType == VersionType.SpecificVersion) + { + if (pkgName.Contains("*")) + { + var exMessage = "Name cannot contain or equal wildcard when using specific version."; + var ex = new ArgumentException(exMessage); + var WildcardError = new ErrorRecord(ex, "InvalidWildCardUsage", ErrorCategory.InvalidOperation, null); + _cmdletPassedIn.WriteError(WildcardError); + + continue; + } + else + { + // Example: Find-PSResource -Name "Az" -Version "3.0.0.0" + // Example: Find-PSResource -Name "Az" -Version "3.0.0.0" -Tag "Windows" + string response = String.Empty; + string tagMsg = String.Empty; + if (_tag.Length == 0) + { + response = currentServer.FindVersion(pkgName, _nugetVersion.ToNormalizedString(), _type, out edi); + } + else + { + response = currentServer.FindVersionWithTag(pkgName, _nugetVersion.ToNormalizedString(), _tag, _type, out edi); + string tagsAsString = String.Join(", ", _tag); + tagMsg = $" and Tags {tagsAsString}"; + } + + string[] responses = new string[]{ response }; + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindVersionFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + PSResourceResult currentResult = currentResponseUtil.ConvertToPSResourceResult(responses: responses).First(); + + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + string errMsg = $"Package with search criteria: Name {pkgName}, Version {_version} {tagMsg} could not be found due to: {currentResult.errorMsg}."; + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(errMsg), "FindVersionResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + PSResourceInfo foundPkg = currentResult.returnedObject; + parentPkgs.Add(foundPkg); + pkgsFound.Add(Utils.CreateHashSetKey(foundPkg.Name, foundPkg.Version.ToString())); + yield return foundPkg; + } + } + else + { + // version type is Version Range + if (pkgName.Contains("*")) + { + var exMessage = "Name cannot contain or equal wildcard when using version range"; + var ex = new ArgumentException(exMessage); + var WildcardError = new ErrorRecord(ex, "InvalidWildCardUsage", ErrorCategory.InvalidOperation, null); + _cmdletPassedIn.WriteError(WildcardError); + } + else + { + // Example: Find-PSResource -Name "Az" -Version "[1.0.0.0, 3.0.0.0]" + string[] responses = Utils.EmptyStrArray; + if (_tag.Length == 0) + { + responses = currentServer.FindVersionGlobbing(pkgName, _versionRange, _prerelease, _type, getOnlyLatest: false, out edi); + } + else + { + var exMessage = "Name cannot contain or equal wildcard when using version range"; + var ex = new ArgumentException(exMessage); + var WildcardError = new ErrorRecord(ex, "InvalidWildCardUsage", ErrorCategory.InvalidOperation, null); + _cmdletPassedIn.WriteError(WildcardError); + continue; + } + + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "FindVersionGlobbingFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + foreach (PSResourceResult currentResult in currentResponseUtil.ConvertToPSResourceResult(responses: responses)) + { + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + string errMsg = $"Package with search criteria: Name {pkgName} and Version {_version} could not be found due to: {currentResult.errorMsg} originating at method FindVersionGlobbingResponseConversionFail()."; + _cmdletPassedIn.WriteWarning(errMsg); + continue; + } + + PSResourceInfo foundPkg = currentResult.returnedObject; + parentPkgs.Add(foundPkg); + pkgsFound.Add(Utils.CreateHashSetKey(foundPkg.Name, foundPkg.Version.ToString())); + yield return foundPkg; + } + } + } } - else if (repositoryCredentialInfo != null) + + // After retrieving all packages find their dependencies + if (_includeDependencies) { - PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( - repositoryName, - repositoryCredentialInfo, - _cmdletPassedIn); - - string password = new NetworkCredential(string.Empty, repoCredential.Password).Password; - source.Credentials = PackageSourceCredential.FromUserInput(repositoryUri.ToString(), repoCredential.UserName, password, true, null); - _cmdletPassedIn.WriteVerbose("credential successfully read from vault and set for repository: " + repositoryName); + if (currentServer.repository.ApiVersion == PSRepositoryInfo.APIVersion.v3) + { + _cmdletPassedIn.WriteWarning("Installing dependencies is not currently supported for V3 server protocol repositories. The package will be installed without installing dependencies."); + yield break; + } + + foreach (PSResourceInfo currentPkg in parentPkgs) + { + foreach (PSResourceInfo pkgDep in HttpFindDependencyPackages(currentServer, currentResponseUtil, currentPkg, repository, pkgsFound)) + { + yield return pkgDep; + } + } } + } - // GetCoreV3() API is able to handle V2 and V3 repository endpoints. - var provider = FactoryExtensionsV3.GetCoreV3(NuGet.Protocol.Core.Types.Repository.Provider); - SourceRepository repository = new SourceRepository(source, provider); - resourceSearch = null; - resourceMetadata = null; + private bool IsTagMatch(PSResourceInfo pkg) + { + List matchedTags = _tag.Intersect(pkg.Tags, StringComparer.InvariantCultureIgnoreCase).ToList(); - try + foreach (string tag in matchedTags) { - resourceSearch = repository.GetResourceAsync().GetAwaiter().GetResult(); - resourceMetadata = repository.GetResourceAsync().GetAwaiter().GetResult(); + _tagsLeftToFind.Remove(tag); } - catch (Exception e) + + return matchedTags.Count > 0; + } + + #endregion + + #region Internal HTTP Client Search Methods + internal IEnumerable HttpFindDependencyPackages( + ServerApiCall currentServer, + ResponseUtil currentResponseUtil, + PSResourceInfo currentPkg, + PSRepositoryInfo repository, + HashSet foundPkgs) + { + if (currentPkg.Dependencies.Length > 0) { - Utils.WriteVerboseOnCmdlet(_cmdletPassedIn, "Error retrieving resource from repository: " + e.Message); + foreach (var dep in currentPkg.Dependencies) + { + PSResourceInfo depPkg = null; + + if (dep.VersionRange == VersionRange.All) + { + string response = currentServer.FindName(dep.Name, _prerelease, _type, out ExceptionDispatchInfo edi); + string[] responses = new string[] { response }; + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "HttpFindDepPackagesFindNameFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + PSResourceResult currentResult = currentResponseUtil.ConvertToPSResourceResult(responses: responses).First(); + + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(currentResult.errorMsg), "FindNameForDepResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + depPkg = currentResult.returnedObject; + string pkgHashKey = Utils.CreateHashSetKey(depPkg.Name, depPkg.Version.ToString()); + + if (!foundPkgs.Contains(pkgHashKey)) + { + foreach (PSResourceInfo depRes in HttpFindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository, foundPkgs)) + { + yield return depRes; + } + } + } + else + { + string[] responses = currentServer.FindVersionGlobbing(dep.Name, dep.VersionRange, _prerelease, ResourceType.None, getOnlyLatest: true, out ExceptionDispatchInfo edi); + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "HttpFindDepPackagesFindVersionGlobbingFail", ErrorCategory.InvalidOperation, this)); + continue; + } + + foreach (PSResourceResult currentResult in currentResponseUtil.ConvertToPSResourceResult(responses: responses)) + { + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + _cmdletPassedIn.WriteError(new ErrorRecord(new PSInvalidOperationException(currentResult.errorMsg), "FindVersionGlobbingForDepResponseConversionFail", ErrorCategory.NotSpecified, this)); + continue; + } + + depPkg = currentResult.returnedObject; + } + + string pkgHashKey = Utils.CreateHashSetKey(depPkg.Name, depPkg.Version.ToString()); + + if (!foundPkgs.Contains(pkgHashKey)) + { + foreach (PSResourceInfo depRes in HttpFindDependencyPackages(currentServer, currentResponseUtil, depPkg, repository, foundPkgs)) + { + yield return depRes; + } + } + } + } } - if (resourceSearch == null || resourceMetadata == null) + string currentPkgHashKey = Utils.CreateHashSetKey(currentPkg.Name, currentPkg.Version.ToString()); + + if (!foundPkgs.Contains(currentPkgHashKey)) { - yield break; + yield return currentPkg; } + } + - filter = new SearchFilter(_prerelease); - context = new SourceCacheContext(); + #endregion - foreach(PSResourceInfo pkg in SearchAcrossNamesInRepository( - repositoryName: String.Equals(repositoryUri.AbsoluteUri, _psGalleryUri, StringComparison.InvariantCultureIgnoreCase) ? _psGalleryRepoName : - (String.Equals(repositoryUri.AbsoluteUri, _poshTestGalleryUri, StringComparison.InvariantCultureIgnoreCase) ? _poshTestGalleryRepoName : repositoryName), - pkgSearchResource: resourceSearch, - pkgMetadataResource: resourceMetadata, - searchFilter: filter, - sourceContext: context)) + #region Private NuGet APIs for Local Repo + + private IEnumerable SearchFromLocalRepository(PSRepositoryInfo repositoryInfo) + { + PackageSearchResource resourceSearch; + PackageMetadataResource resourceMetadata; + SearchFilter filter; + SourceCacheContext context; + + // File based Uri scheme. + if (repositoryInfo.Uri.Scheme == Uri.UriSchemeFile) { - yield return pkg; + FindLocalPackagesResourceV2 localResource = new FindLocalPackagesResourceV2(repositoryInfo.Uri.ToString()); + resourceSearch = new LocalPackageSearchResource(localResource); + resourceMetadata = new LocalPackageMetadataResource(localResource); + filter = new SearchFilter(_prerelease); + context = new SourceCacheContext(); + + foreach (PSResourceInfo pkg in SearchAcrossNamesInRepository( + repositoryName: repositoryInfo.Name, + pkgSearchResource: resourceSearch, + pkgMetadataResource: resourceMetadata, + searchFilter: filter, + sourceContext: context)) + { + yield return pkg; + } + yield break; } } private IEnumerable SearchAcrossNamesInRepository( - string repositoryName, - PackageSearchResource pkgSearchResource, - PackageMetadataResource pkgMetadataResource, - SearchFilter searchFilter, - SourceCacheContext sourceContext) + string repositoryName, + PackageSearchResource pkgSearchResource, + PackageMetadataResource pkgMetadataResource, + SearchFilter searchFilter, + SourceCacheContext sourceContext) { foreach (string pkgName in _pkgsLeftToFind.ToArray()) { @@ -309,6 +862,7 @@ private IEnumerable SearchAcrossNamesInRepository( continue; } + // call NuGet client API foreach (PSResourceInfo pkg in FindFromPackageSourceSearchAPI( repositoryName: repositoryName, pkgName: pkgName, @@ -400,35 +954,26 @@ private IEnumerable FindFromPackageSourceSearchAPI( } else { - if (_isADOFeedRepository) - { - _cmdletPassedIn.WriteError(new ErrorRecord( - new ArgumentException(String.Format("Searching through ADOFeed with wildcard in Name is not supported, so {0} repository will be skipped.", repositoryName)), - "CannotSearchADOFeedWithWildcardName", - ErrorCategory.InvalidArgument, - this)); - yield break; - } - // Case: searching for name containing wildcard i.e "Carbon.*". List wildcardPkgs; try { + string wildcardPkgName = string.Empty; // SearchAsync() API returns the latest version only for all packages that match the wild-card name wildcardPkgs = pkgSearchResource.SearchAsync( - searchTerm: pkgName, + searchTerm: wildcardPkgName, filters: searchFilter, skip: 0, take: SearchAsyncMaxTake, log: NullLogger.Instance, cancellationToken: _cancellationToken).GetAwaiter().GetResult().ToList(); - + if (wildcardPkgs.Count > SearchAsyncMaxReturned) { // Get the rest of the packages. wildcardPkgs.AddRange( pkgSearchResource.SearchAsync( - searchTerm: pkgName, + searchTerm: wildcardPkgName, filters: searchFilter, skip: SearchAsyncMaxTake, take: GalleryMax, @@ -552,7 +1097,7 @@ private IEnumerable FindFromPackageSourceSearchAPI( repositoryName: repositoryName, type: _type, errorMsg: out string errorMsg)) - { + { _cmdletPassedIn.WriteError(new ErrorRecord( new PSInvalidOperationException("Error parsing IPackageSearchMetadata to PSResourceInfo with message: " + errorMsg), "IPackageSearchMetadataToPSResourceInfoParsingError", @@ -561,20 +1106,8 @@ private IEnumerable FindFromPackageSourceSearchAPI( yield break; } - if (_type != ResourceType.None) - { - if (_type == ResourceType.Command && !currentPkg.Type.HasFlag(ResourceType.Command)) - { - continue; - } - if (_type == ResourceType.DscResource && !currentPkg.Type.HasFlag(ResourceType.DscResource)) - { - continue; - } - } - // Only going to go in here for the main package, resolve Type and Tag requirements if any, and then find dependencies - if (_tag == null || (_tag != null && IsTagMatch(currentPkg))) + if (_tag == null || _tag.Length == 0 || (_tag != null && _tag.Length > 0 && IsTagMatch(currentPkg))) { yield return currentPkg; @@ -589,16 +1122,10 @@ private IEnumerable FindFromPackageSourceSearchAPI( } } - private bool IsTagMatch(PSResourceInfo pkg) - { - return _tag.Intersect(pkg.Tags, StringComparer.InvariantCultureIgnoreCase).ToList().Count > 0; - } - private List FindDependencyPackages( PSResourceInfo currentPkg, PackageMetadataResource packageMetadataResource, - SourceCacheContext sourceCacheContext - ) + SourceCacheContext sourceCacheContext) { List thoseToAdd = new List(); FindDependencyPackagesHelper(currentPkg, thoseToAdd, packageMetadataResource, sourceCacheContext); @@ -609,10 +1136,9 @@ private void FindDependencyPackagesHelper( PSResourceInfo currentPkg, List thoseToAdd, PackageMetadataResource packageMetadataResource, - SourceCacheContext sourceCacheContext - ) + SourceCacheContext sourceCacheContext) { - foreach(var dep in currentPkg.Dependencies) + foreach (var dep in currentPkg.Dependencies) { List depPkgs = packageMetadataResource.GetMetadataAsync( packageId: dep.Name, @@ -682,7 +1208,7 @@ SourceCacheContext sourceCacheContext } } } - + #endregion } } diff --git a/src/code/FindPSResource.cs b/src/code/FindPSResource.cs index ceb839504..5e8c9c55d 100644 --- a/src/code/FindPSResource.cs +++ b/src/code/FindPSResource.cs @@ -2,10 +2,12 @@ // Licensed under the MIT License. using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using NuGet.Versioning; using System; using System.Collections.Generic; using System.Linq; using System.Management.Automation; +using System.Net; using System.Threading; using Dbg = System.Diagnostics.Debug; @@ -20,13 +22,13 @@ namespace Microsoft.PowerShell.PowerShellGet.Cmdlets /// [Cmdlet(VerbsCommon.Find, "PSResource", - DefaultParameterSetName = ResourceNameParameterSet)] + DefaultParameterSetName = NameParameterSet)] [OutputType(typeof(PSResourceInfo), typeof(PSCommandResourceInfo))] public sealed class FindPSResource : PSCmdlet { #region Members - private const string ResourceNameParameterSet = "ResourceNameParameterSet"; + private const string NameParameterSet = "NameParameterSet"; private const string CommandNameParameterSet = "CommandNameParameterSet"; private const string DscResourceNameParameterSet = "DscResourceNameParameterSet"; private CancellationTokenSource _cancellationTokenSource; @@ -42,46 +44,32 @@ public sealed class FindPSResource : PSCmdlet [SupportsWildcards] [Parameter(Position = 0, ValueFromPipeline = true, - ParameterSetName = ResourceNameParameterSet)] + ParameterSetName = NameParameterSet)] [ValidateNotNullOrEmpty] public string[] Name { get; set; } /// /// Specifies one or more resource types to find. - /// Resource types supported are: Module, Script, Command, DscResource + /// Resource types supported are: Module, Script /// - [Parameter(ParameterSetName = ResourceNameParameterSet)] + [Parameter(ParameterSetName = NameParameterSet)] public ResourceType Type { get; set; } /// /// Specifies the version of the resource to be found and returned. Wildcards are supported. /// - [SupportsWildcards] - [Parameter(ParameterSetName = ResourceNameParameterSet)] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] + [Parameter(ParameterSetName = NameParameterSet)] [ValidateNotNullOrEmpty] public string Version { get; set; } /// /// When specified, includes prerelease versions in search. /// - [Parameter(ParameterSetName = ResourceNameParameterSet)] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] + [Parameter()] public SwitchParameter Prerelease { get; set; } /// - /// Specifies a module resource package name type to search for. Wildcards are supported. - /// - [SupportsWildcards] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] - [ValidateNotNullOrEmpty] - public string[] ModuleName { get; set; } - - /// - /// Specifies a list of command names that searched module packages will provide. + /// Specifies a list of command names that searched module packages will provide. Wildcards are supported. /// [Parameter(Mandatory = true, ParameterSetName = CommandNameParameterSet)] [ValidateNotNullOrEmpty] @@ -97,19 +85,14 @@ public sealed class FindPSResource : PSCmdlet /// /// Filters search results for resources that include one or more of the specified tags. /// - [Parameter(ParameterSetName = ResourceNameParameterSet)] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] + [Parameter(ParameterSetName = NameParameterSet)] [ValidateNotNull] public string[] Tag { get; set; } /// /// Specifies one or more repository names to search. If not specified, search will include all currently registered repositories. /// - [SupportsWildcards] - [Parameter(ParameterSetName = ResourceNameParameterSet)] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] + [Parameter()] [ArgumentCompleter(typeof(RepositoryNameCompleter))] [ValidateNotNullOrEmpty] public string[] Repository { get; set; } @@ -117,29 +100,29 @@ public sealed class FindPSResource : PSCmdlet /// /// Specifies optional credentials to be used when accessing a repository. /// - [Parameter(ParameterSetName = ResourceNameParameterSet)] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] + [Parameter()] public PSCredential Credential { get; set; } /// /// When specified, search will return all matched resources along with any resources the matched resources depends on. /// - [Parameter(ParameterSetName = ResourceNameParameterSet)] - [Parameter(ParameterSetName = CommandNameParameterSet)] - [Parameter(ParameterSetName = DscResourceNameParameterSet)] + [Parameter(ParameterSetName = NameParameterSet)] public SwitchParameter IncludeDependencies { get; set; } #endregion - #region Method overrides + #region Method Overrides protected override void BeginProcessing() { _cancellationTokenSource = new CancellationTokenSource(); + + var networkCred = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; + _findHelper = new FindHelper( cancellationToken: _cancellationTokenSource.Token, - cmdletPassedIn: this); + cmdletPassedIn: this, + networkCredential: networkCred); // Create a repository story (the PSResourceRepository.xml file) if it does not already exist // This is to create a better experience for those who have just installed v3 and want to get up and running quickly @@ -161,7 +144,7 @@ protected override void ProcessRecord() { switch (ParameterSetName) { - case ResourceNameParameterSet: + case NameParameterSet: ProcessResourceNameParameterSet(); break; @@ -181,27 +164,34 @@ protected override void ProcessRecord() #endregion - #region Private methods + #region Private Methods private void ProcessResourceNameParameterSet() { + // only cases where Name is allowed to not be specified is if Type or Tag parameters are if (!MyInvocation.BoundParameters.ContainsKey(nameof(Name))) { - // only cases where Name is allowed to not be specified is if Type or Tag parameters are - if (!MyInvocation.BoundParameters.ContainsKey(nameof(Type)) && !MyInvocation.BoundParameters.ContainsKey(nameof(Tag))) + if (MyInvocation.BoundParameters.ContainsKey(nameof(Tag))) + { + ProcessTags(); + return; + } + else if (MyInvocation.BoundParameters.ContainsKey(nameof(Type))) + { + Name = new string[] {"*"}; + } + else { ThrowTerminatingError( new ErrorRecord( - new PSInvalidOperationException("Name parameter must be provided."), + new PSInvalidOperationException("Name parameter must be provided, unless Tag or Type parameters are used."), "NameParameterNotProvided", ErrorCategory.InvalidOperation, this)); } - - Name = new string[] {"*"}; } - Name = Utils.ProcessNameWildcards(Name, out string[] errorMsgs, out bool nameContainsWildcard); + Name = Utils.ProcessNameWildcards(Name, removeWildcardEntries:false, out string[] errorMsgs, out bool nameContainsWildcard); foreach (string error in errorMsgs) { @@ -217,23 +207,55 @@ private void ProcessResourceNameParameterSet() if (Name.Length == 0) { return; - } + } - List foundPackages = _findHelper.FindByResourceName( + // determine/parse out Version param + VersionType versionType = VersionType.VersionRange; + NuGetVersion nugetVersion = null; + VersionRange versionRange = null; + + if (Version != null) + { + if (!NuGetVersion.TryParse(Version, out nugetVersion)) + { + if (Version.Trim().Equals("*")) + { + versionRange = VersionRange.All; + versionType = VersionType.VersionRange; + } + else if (!VersionRange.TryParse(Version, out versionRange)) + { + WriteError(new ErrorRecord( + new ArgumentException("Argument for -Version parameter is not in the proper format"), + "IncorrectVersionFormat", + ErrorCategory.InvalidArgument, + this)); + return; + } + } + else + { + versionType = VersionType.SpecificVersion; + } + } + else + { + versionType = VersionType.NoVersion; + } + + foreach (PSResourceInfo pkg in _findHelper.FindByResourceName( name: Name, type: Type, + versionRange: versionRange, + nugetVersion: nugetVersion, + versionType: versionType, version: Version, prerelease: Prerelease, tag: Tag, repository: Repository, - credential: Credential, - includeDependencies: IncludeDependencies); - - foreach (var uniquePackageVersion in foundPackages.GroupBy( - m => new {m.Name, m.Version, m.Repository}).Select( - group => group.First())) + includeDependencies: IncludeDependencies)) { - WriteObject(uniquePackageVersion); + WriteObject(pkg); } } @@ -241,24 +263,16 @@ private void ProcessCommandOrDscParameterSet(bool isSearchingForCommands) { var commandOrDSCNamesToSearch = Utils.ProcessNameWildcards( pkgNames: isSearchingForCommands ? CommandName : DscResourceName, + removeWildcardEntries: true, errorMsgs: out string[] errorMsgs, - isContainWildcard: out bool nameContainsWildcard); - - if (nameContainsWildcard) - { - WriteError(new ErrorRecord( - new PSInvalidOperationException("Wilcards are not supported for -CommandName or -DSCResourceName for Find-PSResource. So all CommandName or DSCResourceName entries will be discarded."), - "CommandDSCResourceNameWithWildcardsNotSupported", - ErrorCategory.InvalidArgument, - this)); - return; - } + isContainWildcard: out bool _); + var paramName = isSearchingForCommands ? "CommandName" : "DscResourceName"; foreach (string error in errorMsgs) { WriteError(new ErrorRecord( - new PSInvalidOperationException(error), - "ErrorFilteringCommandDscResourceNamesForUnsupportedWildcards", + new PSInvalidOperationException($"Wildcards are not supported for -{paramName}: {error}"), + "WildcardsUnsupportedForCommandNameorDSCResourceName", ErrorCategory.InvalidArgument, this)); } @@ -270,50 +284,47 @@ private void ProcessCommandOrDscParameterSet(bool isSearchingForCommands) return; } - var moduleNamesToSearch = Utils.ProcessNameWildcards( - pkgNames: ModuleName, - errorMsgs: out string[] moduleErrorMsgs, + foreach (PSCommandResourceInfo cmdPkg in _findHelper.FindCommandOrDscResource( + isSearchingForCommands: isSearchingForCommands, + prerelease: Prerelease, + tag: commandOrDSCNamesToSearch, + repository: Repository)) + { + WriteObject(cmdPkg); + } + } + + private void ProcessTags() + { + var tagsToSearch = Utils.ProcessNameWildcards( + pkgNames: Tag, + removeWildcardEntries: true, + errorMsgs: out string[] errorMsgs, isContainWildcard: out bool _); - foreach (string error in moduleErrorMsgs) + foreach (string error in errorMsgs) { WriteError(new ErrorRecord( - new PSInvalidOperationException(error), - "ErrorFilteringModuleNamesForUnsupportedWildcards", + new PSInvalidOperationException($"Wildcards are not supported for -Tag: {error}"), + "WildcardsUnsupportedForTag", ErrorCategory.InvalidArgument, this)); } - if (moduleNamesToSearch.Length == 0) + // this catches the case where Name wasn't passed in as null or empty, + // but after filtering out unsupported wildcard names there are no elements left in tagsToSearch + if (tagsToSearch.Length == 0) { - moduleNamesToSearch = new string[] {"*"}; + return; } - - List foundPackages = _findHelper.FindByResourceName( - name: moduleNamesToSearch, - type: isSearchingForCommands? ResourceType.Command : ResourceType.DscResource, - version: Version, + + foreach (PSResourceInfo tagPkg in _findHelper.FindTag( + type: Type, prerelease: Prerelease, - tag: Tag, - repository: Repository, - credential: Credential, - includeDependencies: IncludeDependencies); - - // if a single package contains multiple commands we are interested in, return a unique entry for each: - // Command1 , PackageA - // Command2 , PackageA - foreach (string nameToSearch in commandOrDSCNamesToSearch) + tag: tagsToSearch, + repository: Repository)) { - foreach (var package in foundPackages) - { - // this check ensures DSC names provided as a Command name won't get returned mistakenly - // -CommandName "command1", "dsc1" <- (will not return or add DSC name) - if ((isSearchingForCommands && package.Includes.Command.Contains(nameToSearch)) || - (!isSearchingForCommands && package.Includes.DscResource.Contains(nameToSearch))) - { - WriteObject(new PSCommandResourceInfo(nameToSearch, package)); - } - } + WriteObject(tagPkg); } } diff --git a/src/code/GetHelper.cs b/src/code/GetHelper.cs index 49b24bc9b..e612a7e52 100644 --- a/src/code/GetHelper.cs +++ b/src/code/GetHelper.cs @@ -48,7 +48,16 @@ public IEnumerable GetInstalledPackages( foreach (var pkg in pkgs) { // Filter on specific version. - var nugetVersion = new NuGetVersion(pkg.Version); + pkg.AdditionalMetadata.TryGetValue("NormalizedVersion", out string normalizedVersion); + + if (!NuGetVersion.TryParse( + value: normalizedVersion, + version: out NuGetVersion nugetVersion)) + { + _cmdletPassedIn.WriteVerbose(String.Format("Package's normalized version '{0}' cannot be parsed into NuGetVersion.", normalizedVersion)); + yield break; + } + var pkgVersionRange = new VersionRange( minVersion: nugetVersion, includeMinVersion: true, diff --git a/src/code/GetPSResource.cs b/src/code/GetPSResource.cs index f127cd672..543ec41c6 100644 --- a/src/code/GetPSResource.cs +++ b/src/code/GetPSResource.cs @@ -119,7 +119,7 @@ protected override void ProcessRecord() { WriteVerbose("Entering GetPSResource"); - var namesToSearch = Utils.ProcessNameWildcards(Name, out string[] errorMsgs, out bool _); + var namesToSearch = Utils.ProcessNameWildcards(Name, removeWildcardEntries:false, out string[] errorMsgs, out bool _); foreach (string error in errorMsgs) { WriteError(new ErrorRecord( diff --git a/src/code/IServerAPICalls.cs b/src/code/IServerAPICalls.cs new file mode 100644 index 000000000..5c7b6f9a2 --- /dev/null +++ b/src/code/IServerAPICalls.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using NuGet.Versioning; +using System.Net.Http; +using System.Runtime.ExceptionServices; + +public interface IServerAPICalls +{ + #region Methods + /// + /// Find method which allows for searching for all packages from a repository and returns latest version for each. + /// Examples: Search -Repository PSGallery + /// API call: + /// - Include prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsAbsoluteLatestVersion&includePrerelease=true + /// + string[] FindAll(bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for packages with tag from a repository and returns latest version for each. + /// Examples: Search -Tag "JSON" -Repository PSGallery + /// API call: + /// - Include prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsAbsoluteLatestVersion&searchTerm='tag:JSON'&includePrerelease=true + /// + string[] FindTag(string tag, bool includePrerelease, ResourceType _type, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// Examples: Search "PowerShellGet" + /// API call: + /// - No prerelease: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// - Include prerelease: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// Implementation Note: Need to filter further for latest version (prerelease or non-prerelease dependening on user preference) + /// + string FindName(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + string FindNameWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// Examples: Search "PowerShellGet" "[3.0.0.0, 5.0.0.0]" + /// Search "PowerShellGet" "3.*" + /// API Call: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// Implementation note: Returns all versions, including prerelease ones. Later (in the API client side) we'll do filtering on the versions to satisfy what user provided. + /// + string[] FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + string[] FindNameGlobbingWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "PowerShellGet" "2.2.5" + /// API call: http://www.powershellgallery.com/api/v2/Packages(Id='PowerShellGet', Version='2.2.5') + /// + string[] FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ExceptionDispatchInfo edi); + + // + /// Find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "PowerShellGet" "2.2.5" + /// API call: http://www.powershellgallery.com/api/v2/Packages(Id='PowerShellGet', Version='2.2.5') + /// + string FindVersion(string packageName, string version, ResourceType type, out ExceptionDispatchInfo edi); + + string FindVersionWithTag(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi); + + + /// + /// Installs specific package. + /// Name: no wildcard support. + /// Examples: Install "PowerShellGet" + /// Implementation Note: if prerelease: call IFindPSResource.FindName() + /// if not prerelease: https://www.powershellgallery.com/api/v2/package/Id (Returns latest stable) + /// + HttpContent InstallName(string packageName, bool includePrerelease, out ExceptionDispatchInfo edi); + + /// + /// Installs package with specific name and version. + /// Name: no wildcard support. + /// Version: no wildcard support. + /// Examples: Install "PowerShellGet" -Version "3.0.0.0" + /// Install "PowerShellGet" -Version "3.0.0-beta16" + /// API Call: https://www.powershellgallery.com/api/v2/package/Id/version (version can be prerelease) + /// + HttpContent InstallVersion(string packageName, string version, out ExceptionDispatchInfo edi); + + #endregion +} diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 7153ff86c..3ee86bd4e 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -4,7 +4,6 @@ using Microsoft.PowerShell.PowerShellGet.UtilClasses; using MoreLinq.Extensions; using NuGet.Common; -using NuGet.Configuration; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Packaging.PackageExtraction; @@ -19,6 +18,8 @@ using System.Linq; using System.Management.Automation; using System.Net; +using System.Net.Http; +using System.Runtime.ExceptionServices; using System.Text.RegularExpressions; using System.Threading; @@ -40,36 +41,45 @@ internal class InstallHelper private readonly PSCmdlet _cmdletPassedIn; private List _pathsToInstallPkg; private VersionRange _versionRange; + private NuGetVersion _nugetVersion; + private VersionType _versionType; + private string _versionString; private bool _prerelease; private bool _acceptLicense; private bool _quiet; private bool _reinstall; private bool _force; private bool _trustRepository; - private PSCredential _credential; private bool _asNupkg; private bool _includeXml; private bool _noClobber; private bool _authenticodeCheck; private bool _savePkg; List _pathsToSearch; - HashSet _pkgNamesToInstall; + List _pkgNamesToInstall; private string _tmpPath; + private NetworkCredential _networkCredential; + private HashSet _packagesOnMachine; #endregion - #region Public methods + #region Public Methods - public InstallHelper(PSCmdlet cmdletPassedIn) + public InstallHelper(PSCmdlet cmdletPassedIn, NetworkCredential networkCredential) { CancellationTokenSource source = new CancellationTokenSource(); _cancellationToken = source.Token; _cmdletPassedIn = cmdletPassedIn; + _networkCredential = networkCredential; } - public List InstallPackages( + /// + /// This method calls is the starting point for install processes and is called by Install, Update and Save cmdlets. + /// + public IEnumerable InstallPackages( string[] names, VersionRange versionRange, + string versionString, bool prerelease, string[] repository, bool acceptLicense, @@ -78,7 +88,6 @@ public List InstallPackages( bool force, bool trustRepository, bool noClobber, - PSCredential credential, bool asNupkg, bool includeXml, bool skipDependencyCheck, @@ -86,12 +95,14 @@ public List InstallPackages( bool savePkg, List pathsToInstallPkg, ScopeType? scope, - string tmpPath) + string tmpPath, + HashSet pkgsInstalled) { _cmdletPassedIn.WriteVerbose(string.Format("Parameters passed in >>> Name: '{0}'; Version: '{1}'; Prerelease: '{2}'; Repository: '{3}'; " + "AcceptLicense: '{4}'; Quiet: '{5}'; Reinstall: '{6}'; TrustRepository: '{7}'; NoClobber: '{8}'; AsNupkg: '{9}'; IncludeXml '{10}'; SavePackage '{11}'; TemporaryPath '{12}'", string.Join(",", names), versionRange != null ? (versionRange.OriginalString != null ? versionRange.OriginalString : string.Empty) : string.Empty, + versionString != null ? versionString : String.Empty, prerelease.ToString(), repository != null ? string.Join(",", repository) : string.Empty, acceptLicense.ToString(), @@ -104,8 +115,8 @@ public List InstallPackages( savePkg.ToString(), tmpPath ?? string.Empty)); - _versionRange = versionRange; + _versionString = versionString ?? String.Empty; _prerelease = prerelease; _acceptLicense = acceptLicense || force; _authenticodeCheck = authenticodeCheck; @@ -114,16 +125,32 @@ public List InstallPackages( _force = force; _trustRepository = trustRepository || force; _noClobber = noClobber; - _credential = credential; _asNupkg = asNupkg; _includeXml = includeXml; _savePkg = savePkg; _pathsToInstallPkg = pathsToInstallPkg; _tmpPath = tmpPath ?? Path.GetTempPath(); + bool parsedAsNuGetVersion = NuGetVersion.TryParse(_versionString, out _nugetVersion); + if (parsedAsNuGetVersion) + { + _versionType = VersionType.SpecificVersion; + } + else if (!parsedAsNuGetVersion && _versionRange == null || _versionRange == VersionRange.All) + { + // if VersionRange == VersionRange.All then we wish to get latest package only (maps to VersionType.NoVersion) + _versionType = VersionType.NoVersion; + } + else + { + // versionRange is specified + _versionType = VersionType.VersionRange; + } + // Create list of installation paths to search. _pathsToSearch = new List(); - _pkgNamesToInstall = new HashSet(names); + _pkgNamesToInstall = names.ToList(); + _packagesOnMachine = pkgsInstalled; // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable) // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations @@ -138,23 +165,26 @@ public List InstallPackages( } // Go through the repositories and see which is the first repository to have the pkg version available - return ProcessRepositories( + List installedPkgs = ProcessRepositories( repository: repository, trustRepository: _trustRepository, - credential: _credential, skipDependencyCheck: skipDependencyCheck, - scope: scope?? ScopeType.CurrentUser); + scope: scope ?? ScopeType.CurrentUser); + + return installedPkgs; } #endregion #region Private methods - // This method calls iterates through repositories (by priority order) to search for the pkgs to install + /// + /// This method calls iterates through repositories (by priority order) to search for the packages to install. + /// It calls HTTP or NuGet API based install helper methods, according to repository type. + /// private List ProcessRepositories( string[] repository, bool trustRepository, - PSCredential credential, bool skipDependencyCheck, ScopeType scope) { @@ -162,45 +192,75 @@ private List ProcessRepositories( var yesToAll = false; var noToAll = false; - var findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn); + var findHelper = new FindHelper(_cancellationToken, _cmdletPassedIn, _networkCredential); List allPkgsInstalled = new List(); bool sourceTrusted = false; foreach (var repo in listOfRepositories) { - // If no more packages to install, then return - if (_pkgNamesToInstall.Count == 0) return allPkgsInstalled; + // Explicitly passed in Credential takes precedence over repository CredentialInfo. + if (_networkCredential == null && repo.CredentialInfo != null) + { + PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( + repo.Name, + repo.CredentialInfo, + _cmdletPassedIn); + var username = repoCredential.UserName; + var password = repoCredential.Password; + + _networkCredential = new NetworkCredential(username, password); + + _cmdletPassedIn.WriteVerbose("credential successfully read from vault and set for repository: " + repo.Name); + } + + ServerApiCall currentServer = ServerFactory.GetServer(repo, _networkCredential); + ResponseUtil currentResponseUtil = ResponseUtilFactory.GetResponseUtil(repo); + bool installDepsForRepo = skipDependencyCheck; + + // If no more packages to install, then return + if (_pkgNamesToInstall.Count == 0) { + return allPkgsInstalled; + } + string repoName = repo.Name; _cmdletPassedIn.WriteVerbose(string.Format("Attempting to search for packages in '{0}'", repoName)); - // If it can't find the pkg in one repository, it'll look for it in the next repo in the list - var isLocalRepo = repo.Uri.AbsoluteUri.StartsWith(Uri.UriSchemeFile + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase); - - // Finds parent packages and dependencies - List pkgsFromRepoToInstall = findHelper.FindByResourceName( - name: _pkgNamesToInstall.ToArray(), - type: ResourceType.None, - version: _versionRange?.OriginalString, - prerelease: _prerelease, - tag: null, - repository: new string[] { repoName }, - credential: credential, - includeDependencies: !skipDependencyCheck); - if (pkgsFromRepoToInstall.Count == 0) + if (repo.ApiVersion == PSRepositoryInfo.APIVersion.v2 || repo.ApiVersion == PSRepositoryInfo.APIVersion.v3) { - _cmdletPassedIn.WriteVerbose(string.Format("None of the specified resources were found in the '{0}' repository.", repoName)); - continue; + if (repo.Trusted == false && !trustRepository && !_force) + { + _cmdletPassedIn.WriteVerbose("Checking if untrusted repository should be used"); + + if (!(yesToAll || noToAll)) + { + // Prompt for installation of package from untrusted repository + var message = string.Format(CultureInfo.InvariantCulture, MsgInstallUntrustedPackage, repoName); + sourceTrusted = _cmdletPassedIn.ShouldContinue(message, MsgRepositoryNotTrusted, true, ref yesToAll, ref noToAll); + } + } + + if (!sourceTrusted && !yesToAll) + { + continue; + } + + if ((repo.ApiVersion == PSRepositoryInfo.APIVersion.v3) && (!installDepsForRepo)) + { + _cmdletPassedIn.WriteWarning("Installing dependencies is not currently supported for V3 server protocol repositories. The package will be installed without installing dependencies."); + installDepsForRepo = true; + } + + return HttpInstall(_pkgNamesToInstall.ToArray(), repo, currentServer, currentResponseUtil, scope, skipDependencyCheck, findHelper); } else { - // Check trust for repository where package was found. // Source is only trusted if it's set at the repository level to be trusted, -TrustRepository flag is true, -Force flag is true // OR the user issues trust interactively via console. if (repo.Trusted == false && !trustRepository && !_force) { - _cmdletPassedIn.WriteVerbose(string.Format("Checking if untrusted repository '{0}' should be used", repoName)); + _cmdletPassedIn.WriteVerbose("Checking if untrusted repository should be used"); if (!(yesToAll || noToAll)) { @@ -208,71 +268,92 @@ private List ProcessRepositories( var message = string.Format(CultureInfo.InvariantCulture, MsgInstallUntrustedPackage, repoName); sourceTrusted = _cmdletPassedIn.ShouldContinue(message, MsgRepositoryNotTrusted, true, ref yesToAll, ref noToAll); } + } - if (sourceTrusted || yesToAll) - { - _cmdletPassedIn.WriteVerbose(string.Format("Untrusted repository '{0}' accepted as trusted source.", repoName)); - } - else - { - continue; - } + if (!sourceTrusted && !yesToAll) + { + continue; } - } - // Select the first package from each name group, which is guaranteed to be the latest version. - // We should only have one version returned for each package name - // e.g.: - // PackageA (version 1.0) - // PackageB (version 2.0) - // PackageC (version 1.0) - pkgsFromRepoToInstall = pkgsFromRepoToInstall.GroupBy( - m => new { m.Name }).Select( - group => group.First()).ToList(); + _cmdletPassedIn.WriteVerbose("Untrusted repository accepted as trusted source."); + + // If it can't find the pkg in one repository, it'll look for it in the next repo in the list + var isLocalRepo = repo.Uri.AbsoluteUri.StartsWith(Uri.UriSchemeFile + Uri.SchemeDelimiter, StringComparison.OrdinalIgnoreCase); + + // Finds parent packages and dependencies + List pkgsFromRepoToInstall = findHelper.FindByResourceName( + name: _pkgNamesToInstall.ToArray(), + type: ResourceType.None, + versionRange: _versionRange, + nugetVersion: _nugetVersion, + versionType: _versionType, + version: _versionRange?.OriginalString, + prerelease: _prerelease, + tag: null, + repository: new string[] { repoName }, + includeDependencies: !installDepsForRepo).ToList(); + + if (pkgsFromRepoToInstall.Count == 0) + { + _cmdletPassedIn.WriteVerbose(string.Format("None of the specified resources were found in the '{0}' repository.", repoName)); + continue; + } - // Check to see if the pkgs (including dependencies) are already installed (ie the pkg is installed and the version satisfies the version range provided via param) - if (!_reinstall) - { - pkgsFromRepoToInstall = FilterByInstalledPkgs(pkgsFromRepoToInstall); - } + // Select the first package from each name group, which is guaranteed to be the latest version. + // We should only have one version returned for each package name + // e.g.: + // PackageA (version 1.0) + // PackageB (version 2.0) + // PackageC (version 1.0) + // pkgsFromRepoToInstall = pkgsFromRepoToInstall.GroupBy( + // m => new { m.Name }).Select( + // group => group.First()).ToList(); + + // Check to see if the pkgs (including dependencies) are already installed (ie the pkg is installed and the version satisfies the version range provided via param) + if (!_reinstall) + { + pkgsFromRepoToInstall = FilterByInstalledPkgs(pkgsFromRepoToInstall); + } - if (pkgsFromRepoToInstall.Count is 0) - { - continue; - } + if (pkgsFromRepoToInstall.Count is 0) + { + continue; + } - List pkgsInstalled = InstallPackage( - pkgsFromRepoToInstall, - repoName, - repo.Uri.AbsoluteUri, - repo.CredentialInfo, - credential, - isLocalRepo, - scope: scope); + List pkgsInstalled = InstallPackage( + pkgsFromRepoToInstall, + repoName, + repo.Uri.AbsoluteUri, + repo.CredentialInfo, + isLocalRepo, + scope: scope); - foreach (PSResourceInfo pkg in pkgsInstalled) - { - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } + foreach (PSResourceInfo pkg in pkgsInstalled) + { + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + } - allPkgsInstalled.AddRange(pkgsInstalled); - } + allPkgsInstalled.AddRange(pkgsInstalled); + } - // At this only package names left were those which could not be found in registered repositories - foreach (string pkgName in _pkgNamesToInstall) - { - string message = !sourceTrusted ? $"Package '{pkgName}' with requested version range '{_versionRange.ToString()}' could not be found in any trusted repositories" : - $"Package '{pkgName}' with requested version range '{_versionRange.ToString()}' could not be installed as it was not found in any registered repositories"; + // At this only package names left were those which could not be found in registered repositories + foreach (string pkgName in _pkgNamesToInstall) + { + string message = !sourceTrusted ? $"Package '{pkgName}' with requested version range '{_versionRange.ToString()}' could not be found in any trusted repositories" : + $"Package '{pkgName}' with requested version range '{_versionRange.ToString()}' could not be installed as it was not found in any registered repositories"; - var ex = new ArgumentException(message); - var ResourceNotFoundError = new ErrorRecord(ex, "ResourceNotFoundError", ErrorCategory.ObjectNotFound, null); - _cmdletPassedIn.WriteError(ResourceNotFoundError); + var ex = new ArgumentException(message); + var ResourceNotFoundError = new ErrorRecord(ex, "ResourceNotFoundError", ErrorCategory.ObjectNotFound, null); + _cmdletPassedIn.WriteError(ResourceNotFoundError); + } } return allPkgsInstalled; } - // Check if any of the pkg versions are already installed, if they are we'll remove them from the list of packages to install + /// + /// Checks if any of the package versions are already installed and if they are removes them from the list of packages to install. + /// private List FilterByInstalledPkgs(List packages) { // Package install paths. @@ -310,22 +391,15 @@ private List FilterByInstalledPkgs(List packages } else { - // Only write warning if the package is not a dependency package being installed. - if (_pkgNamesToInstall.Contains(pkg.Name)) { - _cmdletPassedIn.WriteWarning( - string.Format("Resource '{0}' with version '{1}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter", - pkg.Name, - pkg.Version)); - } - else { - _cmdletPassedIn.WriteVerbose( - string.Format("Dependency resource '{0}' with version '{1}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter", - pkg.Name, - pkg.Version)); - } + // Remove from tracking list of packages to install. + pkg.AdditionalMetadata.TryGetValue("NormalizedVersion", out string normalizedVersion); + _cmdletPassedIn.WriteWarning( + string.Format("Resource '{0}' with version '{1}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter", + pkg.Name, + normalizedVersion)); // Remove from tracking list of packages to install. - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); } } @@ -333,14 +407,938 @@ private List FilterByInstalledPkgs(List packages } /// - /// Install provided list of packages, which include Dependent packages if requested. + /// Deletes temp directory and is called at end of install process. + /// + private bool TryDeleteDirectory( + string tempInstallPath, + out ErrorRecord errorMsg) + { + errorMsg = null; + + try + { + Utils.DeleteDirectory(tempInstallPath); + } + catch (Exception e) + { + var TempDirCouldNotBeDeletedError = new ErrorRecord(e, "errorDeletingTempInstallPath", ErrorCategory.InvalidResult, null); + errorMsg = TempDirCouldNotBeDeletedError; + return false; + } + + return true; + } + + /// + /// Moves file from the temp install path to desination path for install. + /// + private void MoveFilesIntoInstallPath( + PSResourceInfo pkgInfo, + bool isModule, + bool isLocalRepo, + string dirNameVersion, + string tempInstallPath, + string installPath, + string newVersion, + string moduleManifestVersion, + string scriptPath) + { + // Creating the proper installation path depending on whether pkg is a module or script + var newPathParent = isModule ? Path.Combine(installPath, pkgInfo.Name) : installPath; + var finalModuleVersionDir = isModule ? Path.Combine(installPath, pkgInfo.Name, moduleManifestVersion) : installPath; + + // If script, just move the files over, if module, move the version directory over + var tempModuleVersionDir = (!isModule || isLocalRepo) ? dirNameVersion + : Path.Combine(tempInstallPath, pkgInfo.Name.ToLower(), newVersion); + + _cmdletPassedIn.WriteVerbose(string.Format("Installation source path is: '{0}'", tempModuleVersionDir)); + _cmdletPassedIn.WriteVerbose(string.Format("Installation destination path is: '{0}'", finalModuleVersionDir)); + + if (isModule) + { + // If new path does not exist + if (!Directory.Exists(newPathParent)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); + Directory.CreateDirectory(newPathParent); + Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); + } + else + { + _cmdletPassedIn.WriteVerbose(string.Format("Temporary module version directory is: '{0}'", tempModuleVersionDir)); + + if (Directory.Exists(finalModuleVersionDir)) + { + // Delete the directory path before replacing it with the new module. + // If deletion fails (usually due to binary file in use), then attempt restore so that the currently + // installed module is not corrupted. + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete with restore on failure.'{0}'", finalModuleVersionDir)); + Utils.DeleteDirectoryWithRestore(finalModuleVersionDir); + } + + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); + Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); + } + } + else if (_asNupkg) + { + foreach (string file in Directory.GetFiles(tempInstallPath)) + { + string fileName = Path.GetFileName(file); + string newFileName = string.Equals(Path.GetExtension(file), ".zip", StringComparison.OrdinalIgnoreCase) ? + $"{Path.GetFileNameWithoutExtension(file)}.nupkg" : fileName; + + Utils.MoveFiles(Path.Combine(tempInstallPath, fileName), Path.Combine(installPath, newFileName)); + } + } + else + { + if (!_savePkg) + { + // Need to delete old xml files because there can only be 1 per script + var scriptXML = pkgInfo.Name + "_InstalledScriptInfo.xml"; + _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)))); + if (File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML))) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting script metadata XML")); + File.Delete(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); + } + + _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML))); + Utils.MoveFiles(Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); + + // Need to delete old script file, if that exists + _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)))); + if (File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt))) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting script file")); + File.Delete(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)); + } + } + + _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt))); + Utils.MoveFiles(scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)); + } + } + + #endregion + + #region Private HTTP Methods + + /// + /// Iterates through package names passed in and calls method to install each package and their dependencies. + /// + private List HttpInstall( + string[] pkgNamesToInstall, + PSRepositoryInfo repository, + ServerApiCall currentServer, + ResponseUtil currentResponseUtil, + ScopeType scope, + bool skipDependencyCheck, + FindHelper findHelper) + { + List pkgsSuccessfullyInstalled = new List(); + + // Install parent package to the temp directory, + // Get the dependencies from the installed package, + // Install all dependencies to temp directory. + // If a single dependency fails to install, roll back by deleting the temp directory. + foreach (var parentPackage in pkgNamesToInstall) + { + string tempInstallPath = CreateInstallationTempPath(); + + try + { + // Hashtable has the key as the package name + // and value as a Hashtable of specific package info: + // packageName, { version = "", isScript = "", isModule = "", pkg = "", etc. } + // Install parent package to the temp directory. + Hashtable packagesHash = HttpInstallPackage( + searchVersionType: _versionType, + specificVersion: _nugetVersion, + versionRange: _versionRange, + pkgNameToInstall: parentPackage, + repository: repository, + currentServer: currentServer, + currentResponseUtil: currentResponseUtil, + tempInstallPath: tempInstallPath, + packagesHash: new Hashtable(StringComparer.InvariantCultureIgnoreCase), + edi: out ExceptionDispatchInfo edi); + + // At this point parent package is installed to temp path. + if (edi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(edi.SourceException, "InstallPackageFailure", ErrorCategory.InvalidOperation, this)); + continue; + } + + if (packagesHash.Count == 0) { + continue; + } + + Hashtable parentPkgInfo = packagesHash[parentPackage] as Hashtable; + PSResourceInfo parentPkgObj = parentPkgInfo["psResourceInfoPkg"] as PSResourceInfo; + + if (!skipDependencyCheck) + { + if (currentServer.repository.ApiVersion == PSRepositoryInfo.APIVersion.v3) + { + _cmdletPassedIn.WriteWarning("Installing dependencies is not currently supported for V3 server protocol repositories. The package will be installed without installing dependencies."); + } + + HashSet myHash = new HashSet(StringComparer.OrdinalIgnoreCase); + // Get the dependencies from the installed package. + if (parentPkgObj.Dependencies.Length > 0) + { + foreach (PSResourceInfo depPkg in findHelper.HttpFindDependencyPackages(currentServer, currentResponseUtil, parentPkgObj, repository, myHash)) + { + if (String.Equals(depPkg.Name, parentPkgObj.Name, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + NuGetVersion depVersion = null; + if (depPkg.AdditionalMetadata.ContainsKey("NormalizedVersion")) + { + if (!NuGetVersion.TryParse(depPkg.AdditionalMetadata["NormalizedVersion"] as string, out depVersion)) + { + NuGetVersion.TryParse(depPkg.Version.ToString(), out depVersion); + } + } + + packagesHash = HttpInstallPackage( + searchVersionType: VersionType.SpecificVersion, + specificVersion: depVersion, + versionRange: null, + pkgNameToInstall: depPkg.Name, + repository: repository, + currentServer: currentServer, + currentResponseUtil: currentResponseUtil, + tempInstallPath: tempInstallPath, + packagesHash: packagesHash, + edi: out ExceptionDispatchInfo installPackageEdi); + + if (installPackageEdi != null) + { + _cmdletPassedIn.WriteError(new ErrorRecord(installPackageEdi.SourceException, "InstallDependencyPackageFailure", ErrorCategory.InvalidOperation, this)); + continue; + } + } + } + } + + // Parent package and dependencies are now installed to temp directory. + // Try to move all package directories from temp directory to final destination. + if (!TryMoveInstallContent(tempInstallPath, scope, packagesHash)) + { + _cmdletPassedIn.WriteError(new ErrorRecord(new InvalidOperationException(), "InstallPackageTryMoveContentFailure", ErrorCategory.InvalidOperation, this)); + } + else + { + foreach (string pkgName in packagesHash.Keys) + { + Hashtable pkgInfo = packagesHash[pkgName] as Hashtable; + pkgsSuccessfullyInstalled.Add(pkgInfo["psResourceInfoPkg"] as PSResourceInfo); + + // Add each pkg to _packagesOnMachine (ie pkgs fully installed on the machine). + _packagesOnMachine.Add(Utils.CreateHashSetKey(pkgName, pkgInfo["pkgVersion"].ToString())); + } + } + } + finally + { + DeleteInstallationTempPath(tempInstallPath); + } + } + + return pkgsSuccessfullyInstalled; + } + + /// + /// Installs a single package to the temporary path. + /// + private Hashtable HttpInstallPackage( + VersionType searchVersionType, + NuGetVersion specificVersion, + VersionRange versionRange, + string pkgNameToInstall, + PSRepositoryInfo repository, + ServerApiCall currentServer, + ResponseUtil currentResponseUtil, + string tempInstallPath, + Hashtable packagesHash, + out ExceptionDispatchInfo edi) + { + //List packagesToInstall = new List(); + string[] responses = Utils.EmptyStrArray; + edi = null; + + switch (searchVersionType) + { + case VersionType.VersionRange: + responses = currentServer.FindVersionGlobbing(pkgNameToInstall, versionRange, _prerelease, ResourceType.None, getOnlyLatest: true, out ExceptionDispatchInfo findVersionGlobbingEdi); + // Server level globbing API will not populate edi for empty response, so must check for empty response and early out + if (findVersionGlobbingEdi != null || responses.Length == 0) + { + edi = findVersionGlobbingEdi; + return packagesHash; + } + + break; + + case VersionType.SpecificVersion: + string nugetVersionString = specificVersion.ToNormalizedString(); // 3.0.17-beta + + string findVersionResponse = currentServer.FindVersion(pkgNameToInstall, nugetVersionString, ResourceType.None, out ExceptionDispatchInfo findVersionEdi); + responses = new string[] { findVersionResponse }; + if (findVersionEdi != null) + { + edi = findVersionEdi; + return packagesHash; + } + + break; + + default: + // VersionType.NoVersion + string findNameResponse = currentServer.FindName(pkgNameToInstall, _prerelease, ResourceType.None, out ExceptionDispatchInfo findNameEdi); + responses = new string[] { findNameResponse }; + if (findNameEdi != null) + { + edi = findNameEdi; + return packagesHash; + } + + break; + } + + PSResourceResult currentResult = currentResponseUtil.ConvertToPSResourceResult(responses: responses).First(); + + if (!String.IsNullOrEmpty(currentResult.errorMsg)) + { + // V2Server API calls will return non-empty response when package is not found but fail at conversion time + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Package for installation could not be found due to: {currentResult.errorMsg}")); + return packagesHash; + } + + PSResourceInfo pkgToInstall = currentResult.returnedObject; + pkgToInstall.RepositorySourceLocation = repository.Uri.ToString(); + pkgToInstall.AdditionalMetadata.TryGetValue("NormalizedVersion", out string pkgVersion); + + // Check to see if the pkg is already installed (ie the pkg is installed and the version satisfies the version range provided via param) + if (!_reinstall) + { + string currPkgNameVersion = Utils.CreateHashSetKey(pkgToInstall.Name, pkgToInstall.Version.ToString()); + if (_packagesOnMachine.Contains(currPkgNameVersion)) + { + _cmdletPassedIn.WriteWarning( + string.Format("Resource '{0}' with version '{1}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter", + pkgToInstall.Name, + pkgVersion)); + return packagesHash; + } + } + + // Download the package. + string pkgName = pkgToInstall.Name; + HttpContent responseContent; + + if (searchVersionType == VersionType.NoVersion && !_prerelease) + { + responseContent = currentServer.InstallName(pkgName, _prerelease, out ExceptionDispatchInfo installNameEdi); + if (installNameEdi != null) + { + edi = installNameEdi; + return packagesHash; + } + } + else + { + responseContent = currentServer.InstallVersion(pkgName, pkgVersion, out ExceptionDispatchInfo installVersionEdi); + if (installVersionEdi != null) + { + edi = installVersionEdi; + return packagesHash; + } + } + + Hashtable updatedPackagesHash; + ErrorRecord error; + bool installedToTempPathSuccessfully = _asNupkg ? TrySaveNupkgToTempPath(responseContent, tempInstallPath, pkgName, pkgVersion, pkgToInstall, packagesHash, out updatedPackagesHash, out error) : + TryInstallToTempPath(responseContent, tempInstallPath, pkgName, pkgVersion, pkgToInstall, packagesHash, out updatedPackagesHash, out error); + + if (!installedToTempPathSuccessfully) + { + edi = ExceptionDispatchInfo.Capture(error.Exception); + return packagesHash; + } + + return updatedPackagesHash; + } + + /// + /// Creates a temporary path used for installation before moving package to its final location. + /// + private string CreateInstallationTempPath() + { + var tempInstallPath = Path.Combine(_tmpPath, Guid.NewGuid().ToString()); + + try + { + var dir = Directory.CreateDirectory(tempInstallPath); // should check it gets created properly + // To delete file attributes from the existing ones get the current file attributes first and use AND (&) operator + // with a mask (bitwise complement of desired attributes combination). + // TODO: check the attributes and if it's read only then set it + // attribute may be inherited from the parent + // TODO: are there Linux accommodations we need to consider here? + dir.Attributes &= ~FileAttributes.ReadOnly; + } + catch (Exception e) + { + // catch more specific exception first + _cmdletPassedIn.ThrowTerminatingError(new ErrorRecord( + new ArgumentException($"Temporary folder for installation could not be created or set due to: {e.Message}"), + "TempFolderCreationError", + ErrorCategory.InvalidOperation, + this)); + } + + return tempInstallPath; + } + + /// + /// Deletes the temporary path used for intermediary installation. + /// + private void DeleteInstallationTempPath(string tempInstallPath) + { + if (Directory.Exists(tempInstallPath)) + { + // Delete the temp directory and all its contents + _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete '{0}'", tempInstallPath)); + if (!TryDeleteDirectory(tempInstallPath, out ErrorRecord errorMsg)) + { + _cmdletPassedIn.WriteError(errorMsg); + } + else + { + _cmdletPassedIn.WriteVerbose(String.Format("Successfully deleted '{0}'", tempInstallPath)); + } + } + } + + /// + /// Attempts to take installed HTTP response content and move it into a temporary install path on the machine. + /// + private bool TryInstallToTempPath( + HttpContent responseContent, + string tempInstallPath, + string pkgName, + string normalizedPkgVersion, + PSResourceInfo pkgToInstall, + Hashtable packagesHash, + out Hashtable updatedPackagesHash, + out ErrorRecord error) + { + error = null; + updatedPackagesHash = packagesHash; + try + { + var pathToFile = Path.Combine(tempInstallPath, $"{pkgName}.{normalizedPkgVersion}.zip"); + using var content = responseContent.ReadAsStreamAsync().Result; + using var fs = File.Create(pathToFile); + content.Seek(0, System.IO.SeekOrigin.Begin); + content.CopyTo(fs); + fs.Close(); + + // Expand the zip file + var pkgVersion = pkgToInstall.Version.ToString(); + var tempDirNameVersion = Path.Combine(tempInstallPath, pkgName.ToLower(), pkgVersion); + Directory.CreateDirectory(tempDirNameVersion); + System.IO.Compression.ZipFile.ExtractToDirectory(pathToFile, tempDirNameVersion); + + File.Delete(pathToFile); + + var moduleManifest = Path.Combine(tempDirNameVersion, pkgName + PSDataFileExt); + var scriptPath = Path.Combine(tempDirNameVersion, pkgName + PSScriptFileExt); + + bool isModule = File.Exists(moduleManifest); + bool isScript = File.Exists(scriptPath); + + if (!isModule && !isScript) { + scriptPath = ""; + } + + // TODO: add pkg validation when we figure out consistent/defined way to do so + if (_authenticodeCheck && !AuthenticodeSignature.CheckAuthenticodeSignature( + pkgName, + tempDirNameVersion, + _cmdletPassedIn, + out error)) + { + return false; + } + + string installPath = string.Empty; + if (isModule) + { + installPath = _pathsToInstallPkg.Find(path => path.EndsWith("Modules", StringComparison.InvariantCultureIgnoreCase)); + + if (!File.Exists(moduleManifest)) + { + var message = String.Format("{0} package could not be installed with error: Module manifest file: {1} does not exist. This is not a valid PowerShell module.", pkgName, moduleManifest); + var ex = new ArgumentException(message); + error = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + + return false; + } + + if (!Utils.TryReadManifestFile( + manifestFilePath: moduleManifest, + manifestInfo: out Hashtable parsedMetadataHashtable, + error: out Exception manifestReadError)) + { + error = new ErrorRecord( + exception: manifestReadError, + errorId: "ManifestFileReadParseError", + errorCategory: ErrorCategory.ReadError, + this); + + return false; + } + + // Accept License verification + if (!_savePkg && !CallAcceptLicense(pkgToInstall, moduleManifest, tempInstallPath, pkgVersion, out error)) + { + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgToInstall.Name, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + + // If NoClobber is specified, ensure command clobbering does not happen + if (_noClobber && DetectClobber(pkgName, parsedMetadataHashtable, out error)) + { + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + } + else if (isScript) + { + installPath = _pathsToInstallPkg.Find(path => path.EndsWith("Scripts", StringComparison.InvariantCultureIgnoreCase)); + + // is script + if (!PSScriptFileInfo.TryTestPSScriptFile( + scriptFileInfoPath: scriptPath, + parsedScript: out PSScriptFileInfo scriptToInstall, + out ErrorRecord[] parseScriptFileErrors, + out string[] _)) + { + foreach (ErrorRecord parseError in parseScriptFileErrors) + { + _cmdletPassedIn.WriteError(parseError); + } + + var ex = new InvalidOperationException($"PSScriptFile could not be parsed"); + error = new ErrorRecord(ex, "psScriptParseError", ErrorCategory.ReadError, null); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + + return false; + } + } + else + { + // This package is not a PowerShell package (eg a resource from the NuGet Gallery). + installPath = _pathsToInstallPkg.Find(path => path.EndsWith("Modules", StringComparison.InvariantCultureIgnoreCase)); + + _cmdletPassedIn.WriteVerbose($"This resource is not a PowerShell package and will be installed to the modules path: {installPath}."); + isModule = true; + } + + installPath = _savePkg ? _pathsToInstallPkg.First() : installPath; + + DeleteExtraneousFiles(pkgName, tempDirNameVersion); + + if (_includeXml) + { + if (!CreateMetadataXMLFile(tempDirNameVersion, installPath, pkgToInstall, isModule, out error)) + { + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgToInstall.Name, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + } + + if (!updatedPackagesHash.ContainsKey(pkgName)) + { + // Add pkg info to hashtable. + updatedPackagesHash.Add(pkgName, new Hashtable(StringComparer.InvariantCultureIgnoreCase) + { + { "isModule", isModule }, + { "isScript", isScript }, + { "psResourceInfoPkg", pkgToInstall }, + { "tempDirNameVersionPath", tempDirNameVersion }, + { "pkgVersion", pkgVersion }, + { "scriptPath", scriptPath }, + { "installPath", installPath } + }); + } + + return true; + } + catch (Exception e) + { + error = new ErrorRecord( + new PSInvalidOperationException( + message: $"Unable to successfully install package '{pkgName}': '{e.Message}' to temporary installation path.", + innerException: e), + "InstallPackageFailed", + ErrorCategory.InvalidOperation, + _cmdletPassedIn); + + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + } + + /// + /// Attempts to take Http response content and move the .nupkg into a temporary install path on the machine. + /// + private bool TrySaveNupkgToTempPath( + HttpContent responseContent, + string tempInstallPath, + string pkgName, + string normalizedPkgVersion, + PSResourceInfo pkgToInstall, + Hashtable packagesHash, + out Hashtable updatedPackagesHash, + out ErrorRecord error) + { + error = null; + updatedPackagesHash = packagesHash; + + try + { + var pathToFile = Path.Combine(tempInstallPath, $"{pkgName}.{normalizedPkgVersion}.zip"); + using var content = responseContent.ReadAsStreamAsync().Result; + using var fs = File.Create(pathToFile); + content.Seek(0, System.IO.SeekOrigin.Begin); + content.CopyTo(fs); + fs.Close(); + + string installPath = _pathsToInstallPkg.First(); + if (_includeXml) + { + if (!CreateMetadataXMLFile(tempInstallPath, installPath, pkgToInstall, isModule: true, out error)) + { + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + } + + if (!updatedPackagesHash.ContainsKey(pkgName)) + { + // Add pkg info to hashtable. + updatedPackagesHash.Add(pkgName, new Hashtable(StringComparer.InvariantCultureIgnoreCase) + { + { "isModule", "" }, + { "isScript", "" }, + { "psResourceInfoPkg", pkgToInstall }, + { "tempDirNameVersionPath", tempInstallPath }, + { "pkgVersion", "" }, + { "scriptPath", "" }, + { "installPath", installPath } + }); + } + + return true; + } + catch (Exception e) + { + error = new ErrorRecord( + new PSInvalidOperationException( + message: $"Unable to successfully save .nupkg '{pkgName}': '{e.Message}' to temporary installation path.", + innerException: e), + "SaveNupkgFailed", + ErrorCategory.InvalidOperation, + _cmdletPassedIn); + + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + } + + /// + /// Moves package files/directories from the temp install path into the final install path location. + /// + private bool TryMoveInstallContent(string tempInstallPath, ScopeType scope, Hashtable packagesHash) + { + foreach (string pkgName in packagesHash.Keys) + { + Hashtable pkgInfo = packagesHash[pkgName] as Hashtable; + bool isModule = (pkgInfo["isModule"] as bool?) ?? false; + bool isScript = (pkgInfo["isScript"] as bool?) ?? false; + PSResourceInfo pkgToInstall = pkgInfo["psResourceInfoPkg"] as PSResourceInfo; + string tempDirNameVersion = pkgInfo["tempDirNameVersionPath"] as string; + string pkgVersion = pkgInfo["pkgVersion"] as string; + string scriptPath = pkgInfo["scriptPath"] as string; + string installPath = pkgInfo["installPath"] as string; + + try + { + MoveFilesIntoInstallPath( + pkgToInstall, + isModule, + isLocalRepo: false, // false for HTTP repo + tempDirNameVersion, + tempInstallPath, + installPath, + newVersion: pkgVersion, // would not have prerelease label in this string + moduleManifestVersion: pkgVersion, + scriptPath); + + _cmdletPassedIn.WriteVerbose(String.Format("Successfully installed package '{0}' to location '{1}'", pkgName, installPath)); + + if (!_savePkg && isScript) + { + string installPathwithBackSlash = installPath + "\\"; + string envPATHVarValue = Environment.GetEnvironmentVariable("PATH", + scope == ScopeType.CurrentUser ? EnvironmentVariableTarget.User : EnvironmentVariableTarget.Machine); + + if (!envPATHVarValue.Contains(installPath) && !envPATHVarValue.Contains(installPathwithBackSlash)) + { + _cmdletPassedIn.WriteWarning(String.Format(ScriptPATHWarning, scope, installPath)); + } + } + } + catch (Exception e) + { + _cmdletPassedIn.WriteError( + new ErrorRecord( + new PSInvalidOperationException( + message: $"Unable to successfully install package '{pkgName}': '{e.Message}'", + innerException: e), + "InstallPackageFailed", + ErrorCategory.InvalidOperation, + _cmdletPassedIn)); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); + return false; + } + } + + return true; + } + + /// + /// If the package requires license to be accepted, checks if the user has accepted it. + /// + private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion, out ErrorRecord error) + { + error = null; + var requireLicenseAcceptance = false; + var success = true; + + if (File.Exists(moduleManifest)) + { + using (StreamReader sr = new StreamReader(moduleManifest)) + { + var text = sr.ReadToEnd(); + + var pattern = "RequireLicenseAcceptance\\s*=\\s*\\$true"; + var patternToSkip1 = "#\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; + var patternToSkip2 = "\\*\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; + + Regex rgx = new Regex(pattern); + Regex rgxComment1 = new Regex(patternToSkip1); + Regex rgxComment2 = new Regex(patternToSkip2); + if (rgx.IsMatch(text) && !rgxComment1.IsMatch(text) && !rgxComment2.IsMatch(text)) + { + requireLicenseAcceptance = true; + } + } + + // Licesnse agreement processing + if (requireLicenseAcceptance) + { + // If module requires license acceptance and -AcceptLicense is not passed in, display prompt + if (!_acceptLicense) + { + var PkgTempInstallPath = Path.Combine(tempInstallPath, p.Name, newVersion); + var LicenseFilePath = Path.Combine(PkgTempInstallPath, "License.txt"); + + if (!File.Exists(LicenseFilePath)) + { + var exMessage = String.Format("{0} package could not be installed with error: License.txt not found. License.txt must be provided when user license acceptance is required.", p.Name); + var ex = new ArgumentException(exMessage); + var acceptLicenseError = new ErrorRecord(ex, "LicenseTxtNotFound", ErrorCategory.ObjectNotFound, null); + error = acceptLicenseError; + success = false; + return success; + } + + // Otherwise read LicenseFile + string licenseText = System.IO.File.ReadAllText(LicenseFilePath); + var acceptanceLicenseQuery = $"Do you accept the license terms for module '{p.Name}'."; + var message = licenseText + "`r`n" + acceptanceLicenseQuery; + + var title = "License Acceptance"; + var yesToAll = false; + var noToAll = false; + var shouldContinueResult = _cmdletPassedIn.ShouldContinue(message, title, true, ref yesToAll, ref noToAll); + + if (shouldContinueResult || yesToAll) + { + _acceptLicense = true; + } + } + + // Check if user agreed to license terms, if they didn't then throw error, otherwise continue to install + if (!_acceptLicense) + { + var message = String.Format("{0} package could not be installed with error: License Acceptance is required for module '{0}'. Please specify '-AcceptLicense' to perform this operation.", p.Name); + var ex = new ArgumentException(message); + var acceptLicenseError = new ErrorRecord(ex, "ForceAcceptLicense", ErrorCategory.InvalidArgument, null); + error = acceptLicenseError; + success = false; + } + } + } + + return success; + } + + /// + /// If the option for no clobber is specified, ensures that commands or cmdlets are not being clobbered. + /// + private bool DetectClobber(string pkgName, Hashtable parsedMetadataHashtable, out ErrorRecord error) + { + error = null; + bool foundClobber = false; + + // Get installed modules, then get all possible paths + // selectPrereleaseOnly is false because even if Prerelease is true we want to include both stable and prerelease, would never select prerelease only. + GetHelper getHelper = new GetHelper(_cmdletPassedIn); + IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( + name: new string[] { "*" }, + versionRange: VersionRange.All, + pathsToSearch: _pathsToSearch, + selectPrereleaseOnly: false); + + List listOfCmdlets = new List(); + foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) + { + listOfCmdlets.Add(cmdletName as string); + } + + foreach (var pkg in pkgsAlreadyInstalled) + { + List duplicateCmdlets = new List(); + List duplicateCmds = new List(); + // See if any of the cmdlets or commands in the pkg we're trying to install exist within a package that's already installed + if (pkg.Includes.Cmdlet != null && pkg.Includes.Cmdlet.Any()) + { + duplicateCmdlets = listOfCmdlets.Where(cmdlet => pkg.Includes.Cmdlet.Contains(cmdlet)).ToList(); + } + + if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) + { + duplicateCmds = listOfCmdlets.Where(commands => pkg.Includes.Command.Contains(commands, StringComparer.InvariantCultureIgnoreCase)).ToList(); + } + + if (duplicateCmdlets.Any() || duplicateCmds.Any()) + { + duplicateCmdlets.AddRange(duplicateCmds); + + var errMessage = string.Format( + "{1} package could not be installed with error: The following commands are already available on this system: '{0}'. This module '{1}' may override the existing commands. If you still want to install this module '{1}', remove the -NoClobber parameter.", + String.Join(", ", duplicateCmdlets), pkgName); + + var ex = new ArgumentException(errMessage); + var noClobberError = new ErrorRecord(ex, "CommandAlreadyExists", ErrorCategory.ResourceExists, null); + error = noClobberError; + foundClobber = true; + + return foundClobber; + } + } + + return foundClobber; + } + + /// + /// Creates metadata XML file for either module or script package. + /// + private bool CreateMetadataXMLFile(string dirNameVersion, string installPath, PSResourceInfo pkg, bool isModule, out ErrorRecord error) + { + error = null; + bool success = true; + // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" + // Modules will have the metadata file: "PSGetModuleInfo.xml" + var metadataXMLPath = isModule ? Path.Combine(dirNameVersion, "PSGetModuleInfo.xml") + : Path.Combine(dirNameVersion, (pkg.Name + "_InstalledScriptInfo.xml")); + + pkg.InstalledDate = DateTime.Now; + pkg.InstalledLocation = installPath; + + // Write all metadata into metadataXMLPath + if (!pkg.TryWrite(metadataXMLPath, out string writeError)) + { + var message = string.Format("{0} package could not be installed with error: Error parsing metadata into XML: '{1}'", pkg.Name, writeError); + var ex = new ArgumentException(message); + var errorParsingMetadata = new ErrorRecord(ex, "ErrorParsingMetadata", ErrorCategory.ParserError, null); + error = errorParsingMetadata; + success = false; + } + + return success; + } + + /// + /// Clean up and delete extraneous files found from the package during install. + /// + private void DeleteExtraneousFiles(string packageName, string dirNameVersion) + { + // Deleting .nupkg SHA file, .nuspec, and .nupkg after unpacking the module + // since we download as .zip for HTTP calls, we shouldn't have .nupkg* files + // var nupkgSHAToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg.sha512"); + // var nupkgToDelete = Path.Combine(dirNameVersion, pkgIdString + ".nupkg"); + // var nupkgMetadataToDelete = Path.Combine(dirNameVersion, ".nupkg.metadata"); + var nuspecToDelete = Path.Combine(dirNameVersion, packageName + ".nuspec"); + var contentTypesToDelete = Path.Combine(dirNameVersion, "[Content_Types].xml"); + var relsDirToDelete = Path.Combine(dirNameVersion, "_rels"); + var packageDirToDelete = Path.Combine(dirNameVersion, "package"); + + if (File.Exists(nuspecToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", nuspecToDelete)); + File.Delete(nuspecToDelete); + } + if (File.Exists(contentTypesToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", contentTypesToDelete)); + File.Delete(contentTypesToDelete); + } + if (Directory.Exists(relsDirToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", relsDirToDelete)); + Utils.DeleteDirectory(relsDirToDelete); + } + if (Directory.Exists(packageDirToDelete)) + { + _cmdletPassedIn.WriteVerbose(string.Format("Deleting '{0}'", packageDirToDelete)); + Utils.DeleteDirectory(packageDirToDelete); + } + } + + #endregion + + #region Private NuGet API Methods + + /// + /// Install provided list of packages, which include Dependent packages if requested (used for Local repositories) /// private List InstallPackage( List pkgsToInstall, string repoName, string repoUri, PSCredentialInfo repoCredentialInfo, - PSCredential credential, bool isLocalRepo, ScopeType scope) { @@ -389,7 +1387,7 @@ private List InstallPackage( var ex = new ArgumentException(message); var packageIdentityVersionParseError = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); _cmdletPassedIn.WriteError(packageIdentityVersionParseError); - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); continue; } @@ -412,75 +1410,38 @@ private List InstallPackage( // Create the package extraction context PackageExtractionContext packageExtractionContext = new PackageExtractionContext( - packageSaveMode: PackageSaveMode.Nupkg, - xmlDocFileSaveMode: PackageExtractionBehavior.XmlDocFileSaveMode, - clientPolicyContext: null, - logger: NullLogger.Instance); - - // Extracting from .nupkg and placing files into tempInstallPath - result.PackageReader.CopyFiles( - destination: tempInstallPath, - packageFiles: result.PackageReader.GetFiles(), - extractFile: new PackageFileExtractor( - result.PackageReader.GetFiles(), - packageExtractionContext.XmlDocFileSaveMode).ExtractPackageFile, - logger: NullLogger.Instance, - token: _cancellationToken); - result.Dispose(); - } - else - { - /* Download from a non-local repository */ - // Set up NuGet API resource for download - PackageSource source = new PackageSource(repoUri); + packageSaveMode: PackageSaveMode.Nupkg, + xmlDocFileSaveMode: PackageExtractionBehavior.XmlDocFileSaveMode, + clientPolicyContext: null, + logger: NullLogger.Instance); - // Explicitly passed in Credential takes precedence over repository CredentialInfo - if (credential != null) - { - string password = new NetworkCredential(string.Empty, credential.Password).Password; - source.Credentials = PackageSourceCredential.FromUserInput(repoUri, credential.UserName, password, true, null); - } - else if (repoCredentialInfo != null) + if (_asNupkg) { - PSCredential repoCredential = Utils.GetRepositoryCredentialFromSecretManagement( - repoName, - repoCredentialInfo, - _cmdletPassedIn); - - string password = new NetworkCredential(string.Empty, repoCredential.Password).Password; - source.Credentials = PackageSourceCredential.FromUserInput(repoUri, repoCredential.UserName, password, true, null); + _cmdletPassedIn.WriteWarning("Saving resource from local/file based repository with -AsNupkg is not yet implemented feature."); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + continue; } - var provider = FactoryExtensionsV3.GetCoreV3(NuGet.Protocol.Core.Types.Repository.Provider); - SourceRepository repository = new SourceRepository(source, provider); - - /* Download from a non-local repository -- ie server */ - var downloadResource = repository.GetResourceAsync().GetAwaiter().GetResult(); - DownloadResourceResult result = null; - try + else { - result = downloadResource.GetDownloadResourceResultAsync( - identity: pkgIdentity, - downloadContext: new PackageDownloadContext(cacheContext), - globalPackagesFolder: tempInstallPath, + // Extracting from .nupkg and placing files into tempInstallPath + result.PackageReader.CopyFiles( + destination: tempInstallPath, + packageFiles: result.PackageReader.GetFiles(), + extractFile: new PackageFileExtractor( + result.PackageReader.GetFiles(), + packageExtractionContext.XmlDocFileSaveMode).ExtractPackageFile, logger: NullLogger.Instance, - token: _cancellationToken).GetAwaiter().GetResult(); - } - catch (Exception e) - { - _cmdletPassedIn.WriteVerbose(string.Format("Error attempting download: '{0}'", e.Message)); - } - finally - { - // Need to close the .nupkg - if (result != null) result.Dispose(); + token: _cancellationToken); } + result.Dispose(); } + _cmdletPassedIn.WriteVerbose(string.Format("Successfully able to download package from source to: '{0}'", tempInstallPath)); // pkgIdentity.Version.Version gets the version without metadata or release labels. string newVersion = pkgIdentity.Version.ToNormalizedString(); - string normalizedVersionNoPrerelease = newVersion; + string normalizedVersionNoPrerelease = newVersion; // 3.0.17-beta or 2.2.5 if (pkgIdentity.Version.IsPrerelease) { // eg: 2.0.2 @@ -543,7 +1504,7 @@ private List InstallPackage( var ex = new ArgumentException(message); var psdataFileDoesNotExistError = new ErrorRecord(ex, "psdataFileNotExistError", ErrorCategory.ReadError, null); _cmdletPassedIn.WriteError(psdataFileDoesNotExistError); - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); continue; } @@ -569,14 +1530,18 @@ private List InstallPackage( pkg.RepositorySourceLocation = repoUri; // Accept License verification - if (!_savePkg && !CallAcceptLicense(pkg, moduleManifest, tempInstallPath, newVersion)) + if (!_savePkg && !CallAcceptLicense(pkg, moduleManifest, tempInstallPath, newVersion, out ErrorRecord licenseError)) { + _cmdletPassedIn.WriteError(licenseError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); continue; } // If NoClobber is specified, ensure command clobbering does not happen - if (_noClobber && DetectClobber(pkg.Name, parsedMetadataHashtable)) + if (_noClobber && DetectClobber(pkg.Name, parsedMetadataHashtable, out ErrorRecord clobberError)) { + _cmdletPassedIn.WriteError(clobberError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); continue; } } @@ -595,6 +1560,7 @@ out string[] _ _cmdletPassedIn.WriteError(error); } + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); continue; } @@ -612,7 +1578,12 @@ out string[] _ if (_includeXml) { - CreateMetadataXMLFile(tempDirNameVersion, installPath, pkg, isModule); + if (!CreateMetadataXMLFile(tempDirNameVersion, installPath, pkg, isModule, out ErrorRecord createMetadataError)) + { + _cmdletPassedIn.WriteError(createMetadataError); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + continue; + } } MoveFilesIntoInstallPath( @@ -651,7 +1622,7 @@ out string[] _ "InstallPackageFailed", ErrorCategory.InvalidOperation, _cmdletPassedIn)); - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); + _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); } finally { @@ -675,177 +1646,9 @@ out string[] _ return pkgsSuccessfullyInstalled; } - private bool CallAcceptLicense(PSResourceInfo p, string moduleManifest, string tempInstallPath, string newVersion) - { - var requireLicenseAcceptance = false; - var success = true; - - if (File.Exists(moduleManifest)) - { - using (StreamReader sr = new StreamReader(moduleManifest)) - { - var text = sr.ReadToEnd(); - - var pattern = "RequireLicenseAcceptance\\s*=\\s*\\$true"; - var patternToSkip1 = "#\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; - var patternToSkip2 = "\\*\\s*RequireLicenseAcceptance\\s*=\\s*\\$true"; - - Regex rgx = new Regex(pattern); - Regex rgxComment1 = new Regex(patternToSkip1); - Regex rgxComment2 = new Regex(patternToSkip2); - if (rgx.IsMatch(text) && !rgxComment1.IsMatch(text) && !rgxComment2.IsMatch(text)) - { - requireLicenseAcceptance = true; - } - } - - // Licesnse agreement processing - if (requireLicenseAcceptance) - { - // If module requires license acceptance and -AcceptLicense is not passed in, display prompt - if (!_acceptLicense) - { - var PkgTempInstallPath = Path.Combine(tempInstallPath, p.Name, newVersion); - var LicenseFilePath = Path.Combine(PkgTempInstallPath, "License.txt"); - - if (!File.Exists(LicenseFilePath)) - { - var exMessage = String.Format("{0} package could not be installed with error: License.txt not found. License.txt must be provided when user license acceptance is required.", p.Name); - var ex = new ArgumentException(exMessage); - var acceptLicenseError = new ErrorRecord(ex, "LicenseTxtNotFound", ErrorCategory.ObjectNotFound, null); - - _cmdletPassedIn.WriteError(acceptLicenseError); - _pkgNamesToInstall.RemoveWhere(x => x.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)); - success = false; - } - - // Otherwise read LicenseFile - string licenseText = System.IO.File.ReadAllText(LicenseFilePath); - var acceptanceLicenseQuery = $"Do you accept the license terms for module '{p.Name}'."; - var message = licenseText + "`r`n" + acceptanceLicenseQuery; - - var title = "License Acceptance"; - var yesToAll = false; - var noToAll = false; - var shouldContinueResult = _cmdletPassedIn.ShouldContinue(message, title, true, ref yesToAll, ref noToAll); - - if (shouldContinueResult || yesToAll) - { - _acceptLicense = true; - } - } - - // Check if user agreed to license terms, if they didn't then throw error, otherwise continue to install - if (!_acceptLicense) - { - var message = String.Format("{0} package could not be installed with error: License Acceptance is required for module '{0}'. Please specify '-AcceptLicense' to perform this operation.", p.Name); - var ex = new ArgumentException(message); - var acceptLicenseError = new ErrorRecord(ex, "ForceAcceptLicense", ErrorCategory.InvalidArgument, null); - - _cmdletPassedIn.WriteError(acceptLicenseError); - _pkgNamesToInstall.RemoveWhere(x => x.Equals(p.Name, StringComparison.InvariantCultureIgnoreCase)); - success = false; - } - } - } - - return success; - } - - private bool DetectClobber(string pkgName, Hashtable parsedMetadataHashtable) - { - // Get installed modules, then get all possible paths - bool foundClobber = false; - GetHelper getHelper = new GetHelper(_cmdletPassedIn); - // selectPrereleaseOnly is false because even if Prerelease is true we want to include both stable and prerelease, never select prerelease only. - IEnumerable pkgsAlreadyInstalled = getHelper.GetPackagesFromPath( - name: new string[] { "*" }, - versionRange: VersionRange.All, - pathsToSearch: _pathsToSearch, - selectPrereleaseOnly: false); - // User parsed metadata hash. - HashSet cmdletsToInstall = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var cmdletName in parsedMetadataHashtable["CmdletsToExport"] as object[]) - { - cmdletsToInstall.Add(cmdletName as string); - } - - // Exit early if there's no cmdlets in the package to be installed. - if (cmdletsToInstall.Count == 0) - { - return foundClobber; - } - - foreach (var pkg in pkgsAlreadyInstalled) - { - // See if any of the cmdlets or commands in the pkg we're trying to install exist within a package that's already installed. - if (pkg.Includes.Cmdlet != null && pkg.Includes.Cmdlet.Any()) - { - foreach (string cmdlet in pkg.Includes.Cmdlet) - { - if (cmdletsToInstall.Contains(cmdlet)) - { - foundClobber = true; - - break; - } - } - } - - if (pkg.Includes.Command != null && pkg.Includes.Command.Any()) - { - foreach (string command in pkg.Includes.Command) - { - if (cmdletsToInstall.Contains(command)) - { - foundClobber = true; - - break; - } - } - } - - if (foundClobber) { - _cmdletPassedIn.WriteError(new ErrorRecord( - new PSInvalidOperationException( - string.Format("{0} package could not be installed with error: One or more of the package's commands are already available on this system. " + - "This module '{0}' may override the existing commands. If you still want to install this module '{0}', remove the -NoClobber parameter.", - pkgName)), - "CommandAlreadyExists", - ErrorCategory.ResourceExists, - this)); - - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkgName, StringComparison.InvariantCultureIgnoreCase)); - - return foundClobber; - } - } - - return foundClobber; - } - - private void CreateMetadataXMLFile(string dirNameVersion, string installPath, PSResourceInfo pkg, bool isModule) - { - // Script will have a metadata file similar to: "TestScript_InstalledScriptInfo.xml" - // Modules will have the metadata file: "PSGetModuleInfo.xml" - var metadataXMLPath = isModule ? Path.Combine(dirNameVersion, "PSGetModuleInfo.xml") - : Path.Combine(dirNameVersion, (pkg.Name + "_InstalledScriptInfo.xml")); - - pkg.InstalledDate = DateTime.Now; - pkg.InstalledLocation = installPath; - - // Write all metadata into metadataXMLPath - if (!pkg.TryWrite(metadataXMLPath, out string error)) - { - var message = string.Format("{0} package could not be installed with error: Error parsing metadata into XML: '{1}'", pkg.Name, error); - var ex = new ArgumentException(message); - var ErrorParsingMetadata = new ErrorRecord(ex, "ErrorParsingMetadata", ErrorCategory.ParserError, null); - - _cmdletPassedIn.WriteError(ErrorParsingMetadata); - _pkgNamesToInstall.RemoveWhere(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - } - + /// + /// Clean up and delete extraneous files found from the package during install (used for Local repositories). + /// private void DeleteExtraneousFiles(PackageIdentity pkgIdentity, string dirNameVersion) { // Deleting .nupkg SHA file, .nuspec, and .nupkg after unpacking the module @@ -896,104 +1699,6 @@ private void DeleteExtraneousFiles(PackageIdentity pkgIdentity, string dirNameVe } } - private bool TryDeleteDirectory( - string tempInstallPath, - out ErrorRecord errorMsg) - { - errorMsg = null; - - try - { - Utils.DeleteDirectory(tempInstallPath); - } - catch (Exception e) - { - var TempDirCouldNotBeDeletedError = new ErrorRecord(e, "errorDeletingTempInstallPath", ErrorCategory.InvalidResult, null); - errorMsg = TempDirCouldNotBeDeletedError; - return false; - } - - return true; - } - - private void MoveFilesIntoInstallPath( - PSResourceInfo pkgInfo, - bool isModule, - bool isLocalRepo, - string dirNameVersion, - string tempInstallPath, - string installPath, - string newVersion, - string moduleManifestVersion, - string scriptPath) - { - // Creating the proper installation path depending on whether pkg is a module or script - var newPathParent = isModule ? Path.Combine(installPath, pkgInfo.Name) : installPath; - var finalModuleVersionDir = isModule ? Path.Combine(installPath, pkgInfo.Name, moduleManifestVersion) : installPath; - - // If script, just move the files over, if module, move the version directory over - var tempModuleVersionDir = (!isModule || isLocalRepo) ? dirNameVersion - : Path.Combine(tempInstallPath, pkgInfo.Name.ToLower(), newVersion); - - _cmdletPassedIn.WriteVerbose(string.Format("Installation source path is: '{0}'", tempModuleVersionDir)); - _cmdletPassedIn.WriteVerbose(string.Format("Installation destination path is: '{0}'", finalModuleVersionDir)); - - if (isModule) - { - // If new path does not exist - if (!Directory.Exists(newPathParent)) - { - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); - Directory.CreateDirectory(newPathParent); - Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); - } - else - { - _cmdletPassedIn.WriteVerbose(string.Format("Temporary module version directory is: '{0}'", tempModuleVersionDir)); - - if (Directory.Exists(finalModuleVersionDir)) - { - // Delete the directory path before replacing it with the new module. - // If deletion fails (usually due to binary file in use), then attempt restore so that the currently - // installed module is not corrupted. - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to delete with restore on failure.'{0}'", finalModuleVersionDir)); - Utils.DeleteDirectoryWithRestore(finalModuleVersionDir); - } - - _cmdletPassedIn.WriteVerbose(string.Format("Attempting to move '{0}' to '{1}'", tempModuleVersionDir, finalModuleVersionDir)); - Utils.MoveDirectory(tempModuleVersionDir, finalModuleVersionDir); - } - } - else - { - if (!_savePkg) - { - // Need to delete old xml files because there can only be 1 per script - var scriptXML = pkgInfo.Name + "_InstalledScriptInfo.xml"; - _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)))); - if (File.Exists(Path.Combine(installPath, "InstalledScriptInfos", scriptXML))) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting script metadata XML")); - File.Delete(Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); - } - - _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML))); - Utils.MoveFiles(Path.Combine(dirNameVersion, scriptXML), Path.Combine(installPath, "InstalledScriptInfos", scriptXML)); - - // Need to delete old script file, if that exists - _cmdletPassedIn.WriteVerbose(string.Format("Checking if path '{0}' exists: ", File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)))); - if (File.Exists(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt))) - { - _cmdletPassedIn.WriteVerbose(string.Format("Deleting script file")); - File.Delete(Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)); - } - } - - _cmdletPassedIn.WriteVerbose(string.Format("Moving '{0}' to '{1}'", scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt))); - Utils.MoveFiles(scriptPath, Path.Combine(finalModuleVersionDir, pkgInfo.Name + PSScriptFileExt)); - } - } - #endregion } } diff --git a/src/code/InstallPSResource.cs b/src/code/InstallPSResource.cs index 84e563235..d6bec1ea6 100644 --- a/src/code/InstallPSResource.cs +++ b/src/code/InstallPSResource.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using Microsoft.PowerShell.PowerShellGet.UtilClasses; -using Newtonsoft.Json; using NuGet.Versioning; using System; using System.Collections; @@ -10,8 +9,7 @@ using System.IO; using System.Linq; using System.Management.Automation; -using Microsoft.PowerShell.Commands; - +using System.Net; using Dbg = System.Diagnostics.Debug; namespace Microsoft.PowerShell.PowerShellGet.Cmdlets @@ -244,13 +242,14 @@ private enum ResourceFileType private string _requiredResourceFile; private string _requiredResourceJson; private Hashtable _requiredResourceHash; + private HashSet _packagesOnMachine; VersionRange _versionRange; InstallHelper _installHelper; ResourceFileType _resourceFileType; #endregion - #region Method overrides + #region Method Overrides protected override void BeginProcessing() { @@ -259,8 +258,13 @@ protected override void BeginProcessing() RepositorySettings.CheckRepositoryStore(); _pathsToInstallPkg = Utils.GetAllInstallationPaths(this, Scope); + List pathsToSearch = Utils.GetAllResourcePaths(this, Scope); + // Only need to find packages installed if -Reinstall is not passed in + _packagesOnMachine = Reinstall ? new HashSet(StringComparer.CurrentCultureIgnoreCase) : Utils.GetInstalledPackages(pathsToSearch, this); + + var networkCred = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; - _installHelper = new InstallHelper(cmdletPassedIn: this); + _installHelper = new InstallHelper(cmdletPassedIn: this, networkCredential: networkCred); } protected override void ProcessRecord() @@ -433,7 +437,7 @@ protected override void ProcessRecord() #endregion - #region Methods + #region Private Methods private void RequiredResourceHelper(Hashtable reqResourceHash) { @@ -501,7 +505,7 @@ private void RequiredResourceHelper(Hashtable reqResourceHash) private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bool pkgPrerelease, string[] pkgRepository, PSCredential pkgCredential, InstallPkgParams reqResourceParams) { - var inputNameToInstall = Utils.ProcessNameWildcards(pkgNames, out string[] errorMsgs, out bool nameContainsWildcard); + var inputNameToInstall = Utils.ProcessNameWildcards(pkgNames, removeWildcardEntries:false, out string[] errorMsgs, out bool nameContainsWildcard); if (nameContainsWildcard) { WriteError(new ErrorRecord( @@ -537,6 +541,7 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo var installedPkgs = _installHelper.InstallPackages( names: pkgNames, versionRange: pkgVersion, + versionString: Version, prerelease: pkgPrerelease, repository: pkgRepository, acceptLicense: AcceptLicense, @@ -545,7 +550,6 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo force: false, trustRepository: TrustRepository, noClobber: NoClobber, - credential: pkgCredential, asNupkg: false, includeXml: true, skipDependencyCheck: SkipDependencyCheck, @@ -553,7 +557,8 @@ private void ProcessInstallHelper(string[] pkgNames, VersionRange pkgVersion, bo savePkg: false, pathsToInstallPkg: _pathsToInstallPkg, scope: Scope, - tmpPath: _tmpPath); + tmpPath: _tmpPath, + pkgsInstalled: _packagesOnMachine); if (PassThru) { diff --git a/src/code/InstallPkgParams.cs b/src/code/InstallPkgParams.cs index 20000a512..0b656d688 100644 --- a/src/code/InstallPkgParams.cs +++ b/src/code/InstallPkgParams.cs @@ -4,7 +4,6 @@ using Microsoft.PowerShell.PowerShellGet.UtilClasses; using NuGet.Versioning; using System; -using System.Collections; using System.Management.Automation; public class InstallPkgParams @@ -101,5 +100,6 @@ public void SetProperty(string propertyName, string propertyValue, out ErrorReco break; } } + #endregion } \ No newline at end of file diff --git a/src/code/PSGetException.cs b/src/code/PSGetException.cs new file mode 100644 index 000000000..c714d1ac9 --- /dev/null +++ b/src/code/PSGetException.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.PowerShell.PowerShellGet.UtilClasses +{ + public class OperationNotSupportedException : Exception + { + public OperationNotSupportedException(string message) + : base(message) + { + } + + } + + public class V3ResourceNotFoundException : Exception + { + public V3ResourceNotFoundException(string message) + : base (message) + { + } + } + + public class JsonParsingException : Exception + { + public JsonParsingException(string message) + : base (message) + { + } + } + + public class SpecifiedTagsNotFoundException : Exception + { + public SpecifiedTagsNotFoundException(string message) + : base (message) + { + } + } + + public class InvalidOrEmptyResponse : Exception + { + public InvalidOrEmptyResponse(string message) + : base (message) + { + } + } +} diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs index 39b42aadd..226d50e3d 100644 --- a/src/code/PSRepositoryInfo.cs +++ b/src/code/PSRepositoryInfo.cs @@ -11,15 +11,28 @@ namespace Microsoft.PowerShell.PowerShellGet.UtilClasses /// public sealed class PSRepositoryInfo { + #region Enums + + public enum APIVersion + { + Unknown, + v2, + v3, + local + } + + #endregion + #region Constructor - public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo) + public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, APIVersion apiVersion) { Name = name; Uri = uri; Priority = priority; Trusted = trusted; CredentialInfo = credentialInfo; + ApiVersion = apiVersion; } #endregion @@ -52,6 +65,11 @@ public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCred /// public PSCredentialInfo CredentialInfo { get; } + /// + /// the API protocol version for the repository + /// + public APIVersion ApiVersion { get; } + #endregion } } diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index ca6057002..6af95f99b 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -9,6 +9,8 @@ using System.Globalization; using System.Linq; using System.Management.Automation; +using System.Text.Json; +using System.Xml; using Dbg = System.Diagnostics.Debug; @@ -16,24 +18,28 @@ namespace Microsoft.PowerShell.PowerShellGet.UtilClasses { #region Enums - [Flags] public enum ResourceType { - None = 0x0, - Module = 0x1, - Script = 0x2, - Command = 0x4, - DscResource = 0x8 + None, + Module, + Script } public enum VersionType { - Unknown, - MinimumVersion, - RequiredVersion, - MaximumVersion + NoVersion, + SpecificVersion, + VersionRange } + // public enum VersionType + // { + // Unknown, + // MinimumVersion, + // RequiredVersion, + // MaximumVersion + // } + public enum ScopeType { CurrentUser, @@ -44,21 +50,21 @@ public enum ScopeType #region VersionInfo - public sealed class VersionInfo - { - public VersionInfo( - VersionType versionType, - Version versionNum) - { - VersionType = versionType; - VersionNum = versionNum; - } + // public sealed class VersionInfo + // { + // public VersionInfo( + // VersionType versionType, + // Version versionNum) + // { + // VersionType = versionType; + // VersionNum = versionNum; + // } - public VersionType VersionType { get; } - public Version VersionNum { get; } + // public VersionType VersionType { get; } + // public Version VersionNum { get; } - public override string ToString() => $"{VersionType}: {VersionNum}"; - } + // public override string ToString() => $"{VersionType}: {VersionNum}"; + // } #endregion @@ -181,9 +187,8 @@ public sealed class Dependency /// /// Constructor - /// + /// An object describes a package dependency /// - /// Hashtable of PSGet includes public Dependency(string dependencyName, VersionRange dependencyVersionRange) { Name = dependencyName; @@ -202,7 +207,7 @@ public sealed class PSCommandResourceInfo // included by the PSResourceInfo property #region Properties - public string Name { get; } + public string[] Names { get; } public PSResourceInfo ParentResource { get; } @@ -213,11 +218,11 @@ public sealed class PSCommandResourceInfo /// /// Constructor /// - /// Name of the command or DSC resource + /// Name of the command or DSC resource /// the parent module resource the command or dsc resource belongs to - public PSCommandResourceInfo(string name, PSResourceInfo parentResource) + public PSCommandResourceInfo(string[] names, PSResourceInfo parentResource) { - Name = name; + Names = names; ParentResource = parentResource; } @@ -233,27 +238,27 @@ public sealed class PSResourceInfo #region Properties public Dictionary AdditionalMetadata { get; } - public string Author { get; } - public string CompanyName { get; internal set; } - public string Copyright { get; internal set; } - public Dependency[] Dependencies { get; } - public string Description { get; } - public Uri IconUri { get; } + public string Author { get; set; } + public string CompanyName { get; set; } + public string Copyright { get; set; } + public Dependency[] Dependencies { get; set; } + public string Description { get; set; } + public Uri IconUri { get; set; } public ResourceIncludes Includes { get; } - public DateTime? InstalledDate { get; internal set; } - public string InstalledLocation { get; internal set; } - public bool IsPrerelease { get; } - public Uri LicenseUri { get; } - public string Name { get; } + public DateTime? InstalledDate { get; set; } + public string InstalledLocation { get; set; } + public bool IsPrerelease { get; set; } + public Uri LicenseUri { get; set; } + public string Name { get; set; } public string PackageManagementProvider { get; } public string PowerShellGetFormatVersion { get; } public string Prerelease { get; } - public Uri ProjectUri { get; } - public DateTime? PublishedDate { get; } - public string ReleaseNotes { get; internal set; } - public string Repository { get; } - public string RepositorySourceLocation { get; internal set; } - public string[] Tags { get; } + public Uri ProjectUri { get; set; } + public DateTime? PublishedDate { get; set; } + public string ReleaseNotes { get; set; } + public string Repository { get; set; } + public string RepositorySourceLocation { get; set; } + public string[] Tags { get; set; } public ResourceType Type { get; } public DateTime? UpdatedDate { get; } public Version Version { get; } @@ -535,7 +540,7 @@ public static bool TryConvert( licenseUri: ParseMetadataLicenseUri(metadataToParse), name: ParseMetadataName(metadataToParse), packageManagementProvider: null, - powershellGetFormatVersion: null, + powershellGetFormatVersion: null, prerelease: ParsePrerelease(metadataToParse), projectUri: ParseMetadataProjectUri(metadataToParse), publishedDate: ParseMetadataPublishedDate(metadataToParse), @@ -560,6 +565,290 @@ public static bool TryConvert( } } + /// + /// Converts XML entry to PSResourceInfo instance + /// used for V2 Server API call find response conversion to PSResourceInfo object + /// + public static bool TryConvertFromXml( + XmlNode entry, + out PSResourceInfo psGetInfo, + string repositoryName, + out string errorMsg) + { + psGetInfo = null; + errorMsg = String.Empty; + + if (entry == null) + { + errorMsg = "TryConvertXmlToPSResourceInfo: Invalid XmlNodeList object. Object cannot be null."; + return false; + } + + try + { + Hashtable metadata = new Hashtable(StringComparer.InvariantCultureIgnoreCase); + + var childNodes = entry.ChildNodes; + foreach (XmlElement child in childNodes) + { + var key = child.LocalName; + var value = child.InnerText; + + if (key.Equals("Version")) + { + metadata[key] = ParseHttpVersion(value, out string prereleaseLabel); + metadata["Prerelease"] = prereleaseLabel; + } + else if (key.EndsWith("Url")) + { + metadata[key] = ParseHttpUrl(value) as Uri; + } + else if (key.Equals("Tags")) + { + metadata[key] = value.Split(new char[]{' '}); + } + else if (key.Equals("Published")) + { + metadata[key] = ParseHttpDateTime(value); + } + else if (key.Equals("Dependencies")) + { + metadata[key] = ParseHttpDependencies(value); + } + else if (key.Equals("IsPrerelease")) + { + bool.TryParse(value, out bool isPrerelease); + + metadata[key] = isPrerelease; + } + else if (key.Equals("NormalizedVersion")) + { + if (!NuGetVersion.TryParse(value, out NuGetVersion parsedNormalizedVersion)) + { + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"TryReadPSGetInfo: Cannot parse NormalizedVersion"); + + parsedNormalizedVersion = new NuGetVersion("1.0.0.0"); + } + + metadata[key] = parsedNormalizedVersion; + } + else + { + metadata[key] = value; + } + } + + var typeInfo = ParseHttpMetadataType(metadata["Tags"] as string[], out ArrayList commandNames, out ArrayList dscResourceNames); + var resourceHashtable = new Hashtable(); + resourceHashtable.Add(nameof(PSResourceInfo.Includes.Command), new PSObject(commandNames)); + resourceHashtable.Add(nameof(PSResourceInfo.Includes.DscResource), new PSObject(dscResourceNames)); + + var additionalMetadataHashtable = new Dictionary(); + additionalMetadataHashtable.Add("NormalizedVersion", metadata["NormalizedVersion"].ToString()); + + var includes = new ResourceIncludes(resourceHashtable); + + psGetInfo = new PSResourceInfo( + additionalMetadata: additionalMetadataHashtable, + author: metadata["Authors"] as String, + companyName: metadata["CompanyName"] as String, + copyright: metadata["Copyright"] as String, + dependencies: metadata["Dependencies"] as Dependency[], + description: metadata["Description"] as String, + iconUri: metadata["IconUrl"] as Uri, + includes: includes, + installedDate: null, + installedLocation: null, + isPrelease: (bool) metadata["IsPrerelease"], + licenseUri: metadata["LicenseUrl"] as Uri, + name: metadata["Id"] as String, + packageManagementProvider: null, + powershellGetFormatVersion: null, + prerelease: metadata["Prerelease"] as String, + projectUri: metadata["ProjectUrl"] as Uri, + publishedDate: metadata["Published"] as DateTime?, + releaseNotes: metadata["ReleaseNotes"] as String, + repository: repositoryName, + repositorySourceLocation: null, + tags: metadata["Tags"] as string[], + type: typeInfo, + updatedDate: null, + version: metadata["Version"] as Version); + + return true; + } + catch (Exception ex) + { + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"TryConvertFromXml: Cannot parse PSResourceInfo from XmlNode with error: {0}", + ex.Message); + return false; + } + } + + + /// + /// Converts JsonDocument entry to PSResourceInfo instance + /// used for V3 Server API call find response conversion to PSResourceInfo object + /// + public static bool TryConvertFromJson( + JsonDocument pkgJson, + out PSResourceInfo psGetInfo, + string repositoryName, + out string errorMsg) + { + psGetInfo = null; + errorMsg = String.Empty; + + if (pkgJson == null) + { + errorMsg = "TryConvertJsonToPSResourceInfo: Invalid json object. Object cannot be null."; + return false; + } + + try + { + Hashtable metadata = new Hashtable(StringComparer.InvariantCultureIgnoreCase); + JsonElement rootDom = pkgJson.RootElement; + + // Version + if (rootDom.TryGetProperty("version", out JsonElement versionElement)) + { + string versionValue = versionElement.ToString(); + metadata["Version"] = ParseHttpVersion(versionValue, out string prereleaseLabel); + metadata["Prerelease"] = prereleaseLabel; + + if (!NuGetVersion.TryParse(versionValue, out NuGetVersion parsedNormalizedVersion)) + { + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"TryReadPSGetInfo: Cannot parse NormalizedVersion"); + + parsedNormalizedVersion = new NuGetVersion("1.0.0.0"); + } + metadata["NormalizedVersion"] = parsedNormalizedVersion; + } + + // License Url + if (rootDom.TryGetProperty("licenseUrl", out JsonElement licenseUrlElement)) + { + metadata["LicenseUrl"] = ParseHttpUrl(licenseUrlElement.ToString()) as Uri; + } + + // Project Url + if (rootDom.TryGetProperty("projectUrl", out JsonElement projectUrlElement)) + { + metadata["ProjectUrl"] = ParseHttpUrl(projectUrlElement.ToString()) as Uri; + } + + // Tags + if (rootDom.TryGetProperty("tags", out JsonElement tagsElement)) + { + List tags = new List(); + foreach (var tag in tagsElement.EnumerateArray()) + { + tags.Add(tag.ToString()); + } + metadata["Tags"] = tags.ToArray(); + } + + // PublishedDate + if (rootDom.TryGetProperty("published", out JsonElement publishedElement)) + { + metadata["PublishedDate"] = ParseHttpDateTime(publishedElement.ToString()); + } + + // Dependencies + // TODO 3.0.0-beta21, a little complicated + + // IsPrerelease + if (rootDom.TryGetProperty("isPrerelease", out JsonElement isPrereleaseElement)) + { + metadata["IsPrerelease"] = isPrereleaseElement.GetBoolean(); + } + + // Author + if (rootDom.TryGetProperty("authors", out JsonElement authorsElement)) + { + metadata["Authors"] = authorsElement.ToString(); + + // CompanyName + // CompanyName is not provided in v3 pkg metadata response, so we've just set it to the author, + // which is often the company + metadata["CompanyName"] = authorsElement.ToString(); + } + + // Copyright + if (rootDom.TryGetProperty("copyright", out JsonElement copyrightElement)) + { + metadata["Copyright"] = copyrightElement.ToString(); + } + + // Description + if (rootDom.TryGetProperty("description", out JsonElement descriptiontElement)) + { + metadata["Description"] = descriptiontElement.ToString(); + } + + // Id + if (rootDom.TryGetProperty("id", out JsonElement idElement)) + { + metadata["Id"] = idElement.ToString(); + } + + // ReleaseNotes + if (rootDom.TryGetProperty("releaseNotes", out JsonElement releaseNotesElement)) { + metadata["ReleaseNotes"] = releaseNotesElement.ToString(); + } + + var additionalMetadataHashtable = new Dictionary + { + { "NormalizedVersion", metadata["NormalizedVersion"].ToString() } + }; + + psGetInfo = new PSResourceInfo( + additionalMetadata: additionalMetadataHashtable, + author: metadata["Authors"] as String, + companyName: metadata["CompanyName"] as String, + copyright: metadata["Copyright"] as String, + dependencies: metadata["Dependencies"] as Dependency[], + description: metadata["Description"] as String, + iconUri: null, + includes: null, + installedDate: null, + installedLocation: null, + isPrelease: (bool)metadata["IsPrerelease"], + licenseUri: metadata["LicenseUrl"] as Uri, + name: metadata["Id"] as String, + packageManagementProvider: null, + powershellGetFormatVersion: null, + prerelease: metadata["Prerelease"] as String, + projectUri: metadata["ProjectUrl"] as Uri, + publishedDate: metadata["PublishedDate"] as DateTime?, + releaseNotes: metadata["ReleaseNotes"] as String, + repository: repositoryName, + repositorySourceLocation: null, + tags: metadata["Tags"] as string[], + type: ResourceType.None, + updatedDate: null, + version: metadata["Version"] as Version); + + return true; + + } + catch (Exception ex) + { + errorMsg = string.Format( + CultureInfo.InvariantCulture, + @"TryConvertFromJson: Cannot parse PSResourceInfo from json object with error: {0}", + ex.Message); + return false; + } + } + #endregion #region Private static methods @@ -723,6 +1012,8 @@ private static string ConcatenateVersionWithPrerelease(string version, string pr return Utils.GetNormalizedVersionString(version, prerelease); } + #endregion + #region Parse Metadata private static methods private static string ParseMetadataAuthor(IPackageSearchMetadata pkg) @@ -853,13 +1144,11 @@ private static ResourceType ParseMetadataType( if (tag.StartsWith("PSCommand_", StringComparison.InvariantCultureIgnoreCase)) { - currentPkgType |= ResourceType.Command; commandNames.Add(tag.Split('_')[1]); } if (tag.StartsWith("PSDscResource_", StringComparison.InvariantCultureIgnoreCase)) { - currentPkgType |= ResourceType.DscResource; dscResourceNames.Add(tag.Split('_')[1]); } } @@ -877,7 +1166,137 @@ private static Version ParseMetadataVersion(IPackageSearchMetadata pkg) return null; } - #endregion + private static Version ParseHttpVersion(string versionString, out string prereleaseLabel) + { + prereleaseLabel = String.Empty; + + if (!String.IsNullOrEmpty(versionString)) + { + string pkgVersion = versionString; + if (versionString.Contains("-")) + { + // versionString: "1.2.0-alpha1" + string[] versionStringParsed = versionString.Split('-'); + if (versionStringParsed.Length == 1) + { + // versionString: "1.2.0-" (unlikely, at least should not be from our PSResourceInfo.TryWrite()) + pkgVersion = versionStringParsed[0]; + } + else + { + // versionStringParsed.Length > 1 (because string contained '-' so couldn't be 0) + // versionString: "1.2.0-alpha1" + pkgVersion = versionStringParsed[0]; + prereleaseLabel = versionStringParsed[1]; + } + } + + // at this point, version is normalized (i.e either "1.2.0" (if part of prerelease) or "1.2.0.0" otherwise) + // parse the pkgVersion parsed out above into a System.Version object + if (!Version.TryParse(pkgVersion, out Version parsedVersion)) + { + prereleaseLabel = String.Empty; + return null; + } + else + { + return parsedVersion; + } + } + + // version could not be parsed as string, it was written to XML file as a System.Version object + // V3 code briefly did so, I believe so we provide support for it + return new System.Version(); + } + + public static Uri ParseHttpUrl(string uriString) + { + Uri parsedUri; + Uri.TryCreate(uriString, UriKind.Absolute, out parsedUri); + + return parsedUri; + } + + public static DateTime? ParseHttpDateTime(string publishedString) + { + DateTime.TryParse(publishedString, out DateTime parsedDateTime); + return parsedDateTime; + } + + public static Dependency[] ParseHttpDependencies(string dependencyString) + { + /* + Az.Profile:[0.1.0, ):|Az.Aks:[0.1.0, ):|Az.AnalysisServices:[0.1.0, ): + Post 1st Split: + ["Az.Profile:[0.1.0, ):", "Az.Aks:[0.1.0, ):", "Az.AnalysisServices:[0.1.0, ):"] + */ + string[] dependencies = dependencyString.Split(new char[]{'|'}, StringSplitOptions.RemoveEmptyEntries); + + List dependencyList = new List(); + foreach (string dependency in dependencies) + { + /* + The Element: "Az.Profile:[0.1.0, ):" + Post 2nd Split: ["Az.Profile", "[0.1.0, )"] + */ + string[] dependencyParts = dependency.Split(new char[]{':'}, StringSplitOptions.RemoveEmptyEntries); + + VersionRange dependencyVersion; + if (dependencyParts.Length == 1) + { + dependencyVersion = VersionRange.All; + } + else + { + if (!Utils.TryParseVersionOrVersionRange(dependencyParts[1], out dependencyVersion)) + { + dependencyVersion = VersionRange.All; + } + } + + dependencyList.Add(new Dependency(dependencyParts[0], dependencyVersion)); + } + + return dependencyList.ToArray(); + } + + private static ResourceType ParseHttpMetadataType( + string[] tags, + out ArrayList commandNames, + out ArrayList dscResourceNames) + { + // possible type combinations: + // M, C + // M, D + // M + // S + + commandNames = new ArrayList(); + dscResourceNames = new ArrayList(); + + ResourceType pkgType = ResourceType.Module; + foreach (string tag in tags) + { + if(String.Equals(tag, "PSScript", StringComparison.InvariantCultureIgnoreCase)) + { + // clear default Module tag, because a Script resource cannot be a Module resource also + pkgType = ResourceType.Script; + pkgType &= ~ResourceType.Module; + } + + if (tag.StartsWith("PSCommand_", StringComparison.InvariantCultureIgnoreCase)) + { + commandNames.Add(tag.Split('_')[1]); + } + + if (tag.StartsWith("PSDscResource_", StringComparison.InvariantCultureIgnoreCase)) + { + dscResourceNames.Add(tag.Split('_')[1]); + } + } + + return pkgType; + } #endregion diff --git a/src/code/PSResourceResult.cs b/src/code/PSResourceResult.cs new file mode 100644 index 000000000..20526f153 --- /dev/null +++ b/src/code/PSResourceResult.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.PowerShell.PowerShellGet.UtilClasses +{ + public sealed class PSResourceResult + { + internal PSResourceInfo returnedObject { get; set; } + internal PSCommandResourceInfo returnedCmdObject { get; set; } + internal string errorMsg { get; set; } + internal bool isTerminatingError { get; set; } + + + public PSResourceResult(PSResourceInfo returnedObject, string errorMsg, bool isTerminatingError) + { + this.returnedObject = returnedObject; + this.errorMsg = errorMsg; + this.isTerminatingError = isTerminatingError; + } + + + public PSResourceResult(PSCommandResourceInfo returnedCmdObject, string errorMsg, bool isTerminatingError) + { + this.returnedCmdObject = returnedCmdObject; + this.errorMsg = errorMsg; + this.isTerminatingError = isTerminatingError; + } + } +} diff --git a/src/code/PSScriptFileInfo.cs b/src/code/PSScriptFileInfo.cs index da80e196b..c7b9e076d 100644 --- a/src/code/PSScriptFileInfo.cs +++ b/src/code/PSScriptFileInfo.cs @@ -1,8 +1,8 @@ -using System.Collections; // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; +using System.Collections; using System.Collections.Generic; using System.IO; using System.Management.Automation; diff --git a/src/code/PowerShellGet.csproj b/src/code/PowerShellGet.csproj index 664024242..c1c06162f 100644 --- a/src/code/PowerShellGet.csproj +++ b/src/code/PowerShellGet.csproj @@ -9,7 +9,7 @@ 3.0.19 3.0.19 netstandard2.0 - 8.0 + 9.0 @@ -25,6 +25,7 @@ + diff --git a/src/code/PublishPSResource.cs b/src/code/PublishPSResource.cs index 4b9277792..937146d97 100644 --- a/src/code/PublishPSResource.cs +++ b/src/code/PublishPSResource.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Language; +using System.Net; using System.Net.Http; using System.Text.RegularExpressions; using System.Threading; @@ -132,7 +133,7 @@ public PSCredential ProxyCredential { private string pathToModuleManifestToPublish = string.Empty; private string pathToModuleDirToPublish = string.Empty; private ResourceType resourceType = ResourceType.None; - + private NetworkCredential _networkCredential; #endregion #region Method overrides @@ -141,6 +142,8 @@ protected override void BeginProcessing() { _cancellationToken = new CancellationToken(); + _networkCredential = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; + // Create a respository story (the PSResourceRepository.xml file) if it does not already exist // This is to create a better experience for those who have just installed v3 and want to get up and running quickly RepositorySettings.CheckRepositoryStore(); @@ -754,23 +757,43 @@ private bool CheckDependenciesExist(Hashtable dependencies, string repositoryNam depVersion = string.IsNullOrWhiteSpace(depVersion) ? "*" : depVersion; VersionRange versionRange = null; - if (!Utils.TryParseVersionOrVersionRange(depVersion, out versionRange)) + VersionType versionType = VersionType.VersionRange; + NuGetVersion nugetVersion = null; + + if (depVersion != null) { - // This should never be true because Test-ModuleManifest will throw an error if dependency versions are incorrectly formatted - // This is being left as a safeguard for parsing a version from a string to a version range. - ThrowTerminatingError(new ErrorRecord( - new ArgumentException(string.Format("Error parsing dependency version {0}, from the module {1}", depVersion, depName)), - "IncorrectVersionFormat", - ErrorCategory.InvalidArgument, - this)); + if (!NuGetVersion.TryParse(depVersion, out nugetVersion)) + { + if (depVersion.Trim().Equals("*")) + { + versionRange = VersionRange.All; + versionType = VersionType.VersionRange; + } + else if (!VersionRange.TryParse(depVersion, out versionRange)) + { + ThrowTerminatingError(new ErrorRecord( + new ArgumentException("Argument for -Version parameter is not in the proper format"), + "IncorrectVersionFormat", + ErrorCategory.InvalidArgument, + this)); + } + } + else + { + versionType = VersionType.SpecificVersion; + } + } + else + { + versionType = VersionType.NoVersion; } // Search for and return the dependency if it's in the repository. - FindHelper findHelper = new FindHelper(_cancellationToken, this); + FindHelper findHelper = new FindHelper(_cancellationToken, this, _networkCredential); bool depPrerelease = depVersion.Contains("-"); var repository = new[] { repositoryName }; - var dependencyFound = findHelper.FindByResourceName(depName, ResourceType.Module, depVersion, depPrerelease, null, repository, Credential, false); + var dependencyFound = findHelper.FindByResourceName(depName, ResourceType.Module, versionRange, nugetVersion, versionType, depVersion, depPrerelease, null, repository, false); if (dependencyFound == null || !dependencyFound.Any()) { var message = String.Format("Dependency '{0}' was not found in repository '{1}'. Make sure the dependency is published to the repository before publishing this module.", dependency, repositoryName); diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index e5a7cd16b..e79d76fc1 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -9,6 +9,7 @@ using System.Management.Automation; using System.Xml; using System.Xml.Linq; +using System.Xml.XPath; namespace Microsoft.PowerShell.PowerShellGet.UtilClasses { @@ -60,7 +61,7 @@ public static void CheckRepositoryStore() // Add PSGallery to the newly created store Uri psGalleryUri = new Uri(PSGalleryRepoUri); - Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, force: false); + Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, PSRepositoryInfo.APIVersion.v2, force: false); } // Open file (which should exist now), if cannot/is corrupted then throw error @@ -103,6 +104,8 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } + PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(repoUri); + if (repoCredentialInfo != null) { bool isSecretManagementModuleAvailable = Utils.IsSecretManagementModuleAvailable(repoName, cmdletPassedIn); @@ -136,7 +139,7 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - var repo = RepositorySettings.Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, force); + var repo = RepositorySettings.Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion, force); return repo; } @@ -226,7 +229,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr /// Returns: PSRepositoryInfo containing information about the repository just added to the repository store /// /// - public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, bool force) + public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, PSRepositoryInfo.APIVersion apiVersion, bool force) { try { @@ -261,6 +264,7 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit "Repository", new XAttribute("Name", repoName), new XAttribute("Url", repoUri), + new XAttribute("APIVersion", apiVersion), new XAttribute("Priority", repoPriority), new XAttribute("Trusted", repoTrusted) ); @@ -281,7 +285,7 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit throw new PSInvalidOperationException(String.Format("Adding to repository store failed: {0}", e.Message)); } - return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo); + return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion); } /// @@ -317,7 +321,13 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio { errorMsg = $"Repository element does not contain neccessary 'Trusted' attribute, in file located at path: {FullRepositoryPath}. Fix this in your file and run again."; return null; - } + } + + if (node.Attribute("APIVersion") == null) + { + errorMsg = $"Repository element does not contain neccessary 'APIVersion' attribute, in file located at path: {FullRepositoryPath}. Fix this in your file and run again."; + return null; + } bool urlAttributeExists = node.Attribute("Url") != null; bool uriAttributeExists = node.Attribute("Uri") != null; @@ -336,6 +346,7 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio // determine if existing repository node (which we wish to update) had Url or Uri attribute Uri thisUrl = null; + PSRepositoryInfo.APIVersion apiVersion = (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value); if (repoUri != null) { if (!Uri.TryCreate(repoUri.AbsoluteUri, UriKind.Absolute, out thisUrl)) @@ -351,6 +362,8 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio { node.Attribute("Uri").Value = thisUrl.AbsoluteUri; } + + apiVersion = GetRepoAPIVersion(repoUri); } else { @@ -421,7 +434,8 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio thisUrl, Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), - thisCredentialInfo); + thisCredentialInfo, + apiVersion); // Close the file root.Save(FullRepositoryPath); @@ -484,6 +498,12 @@ public static List Remove(string[] repoNames, out string[] err continue; } + if (node.Attribute("APIVersion") == null) + { + tempErrorList.Add(String.Format("Repository element does not contain neccessary 'APIVersion' attribute, in file located at path: {0}. Fix this in your file and run again.", FullRepositoryPath)); + continue; + } + // determine if repo had Url or Uri (less likely) attribute bool urlAttributeExists = node.Attribute("Url") != null; bool uriAttributeExists = node.Attribute("Uri") != null; @@ -499,7 +519,9 @@ public static List Remove(string[] repoNames, out string[] err new Uri(node.Attribute(attributeUrlUriName).Value), Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), - repoCredentialInfo)); + repoCredentialInfo, + (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value))); + // Remove item from file node.Remove(); } @@ -570,6 +592,7 @@ public static List Read(string[] repoNames, out string[] error tempErrorList.Add(String.Format("Unable to read incorrectly formatted Url for repo {0}", repo.Attribute("Name").Value)); continue; } + } else if (uriAttributeExists) { @@ -580,6 +603,15 @@ public static List Read(string[] repoNames, out string[] error } } + if (repo.Attribute("APIVersion") == null) + { + PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + + XElement repoXElem = FindRepositoryElement(doc, repo.Attribute("Name").Value); + repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); + doc.Save(FullRepositoryPath); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {repo.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -616,7 +648,8 @@ public static List Read(string[] repoNames, out string[] error thisUrl, Int32.Parse(repo.Attribute("Priority").Value), Boolean.Parse(repo.Attribute("Trusted").Value), - thisCredentialInfo); + thisCredentialInfo, + (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), repo.Attribute("APIVersion").Value)); foundRepos.Add(currentRepoItem); } @@ -673,6 +706,15 @@ public static List Read(string[] repoNames, out string[] error } } + if (node.Attribute("APIVersion") == null) + { + PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + + XElement repoXElem = FindRepositoryElement(doc, node.Attribute("Name").Value); + repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); + doc.Save(FullRepositoryPath); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {node.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -709,7 +751,8 @@ public static List Read(string[] repoNames, out string[] error thisUrl, Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), - thisCredentialInfo); + thisCredentialInfo, + (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value)); foundRepos.Add(currentRepoItem); } @@ -761,6 +804,26 @@ private static XDocument LoadXDocument(string filePath) return XDocument.Load(xmlReader); } + private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) { + + if (repoUri.AbsoluteUri.EndsWith("api/v2")) + { + return PSRepositoryInfo.APIVersion.v2; + } + else if (repoUri.AbsoluteUri.EndsWith("v3/index.json")) + { + return PSRepositoryInfo.APIVersion.v3; + } + else if (repoUri.Scheme == Uri.UriSchemeFile) + { + return PSRepositoryInfo.APIVersion.local; + } + else + { + return PSRepositoryInfo.APIVersion.Unknown; + } + } + #endregion } } diff --git a/src/code/ResponseUtil.cs b/src/code/ResponseUtil.cs new file mode 100644 index 000000000..be162dc05 --- /dev/null +++ b/src/code/ResponseUtil.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using System.Collections.Generic; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal abstract class ResponseUtil + { + #region Members + + public abstract PSRepositoryInfo repository { get; set; } + + #endregion + + #region Constructor + + public ResponseUtil(PSRepositoryInfo repository) + { + this.repository = repository; + } + + #endregion + + #region Methods + + public abstract IEnumerable ConvertToPSResourceResult(string[] responses); + + #endregion + + } +} \ No newline at end of file diff --git a/src/code/ResponseUtilFactory.cs b/src/code/ResponseUtilFactory.cs new file mode 100644 index 000000000..92baed6f5 --- /dev/null +++ b/src/code/ResponseUtilFactory.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal class ResponseUtilFactory + { + public static ResponseUtil GetResponseUtil(PSRepositoryInfo repository) + { + PSRepositoryInfo.APIVersion repoApiVersion = repository.ApiVersion; + ResponseUtil currentResponseUtil = null; + + switch (repoApiVersion) + { + case PSRepositoryInfo.APIVersion.v2: + currentResponseUtil = new V2ResponseUtil(repository); + break; + + case PSRepositoryInfo.APIVersion.v3: + currentResponseUtil = new V3ResponseUtil(repository); + break; + } + + return currentResponseUtil; + } + } +} \ No newline at end of file diff --git a/src/code/SavePSResource.cs b/src/code/SavePSResource.cs index 14193eaa3..9c28c6205 100644 --- a/src/code/SavePSResource.cs +++ b/src/code/SavePSResource.cs @@ -8,7 +8,7 @@ using System.IO; using System.Linq; using System.Management.Automation; - +using System.Net; using Dbg = System.Diagnostics.Debug; namespace Microsoft.PowerShell.PowerShellGet.Cmdlets @@ -179,7 +179,9 @@ protected override void BeginProcessing() // This is to create a better experience for those who have just installed v3 and want to get up and running quickly RepositorySettings.CheckRepositoryStore(); - _installHelper = new InstallHelper(cmdletPassedIn: this); + var networkCred = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; + + _installHelper = new InstallHelper(cmdletPassedIn: this, networkCredential: networkCred); } protected override void ProcessRecord() @@ -237,7 +239,7 @@ protected override void ProcessRecord() private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] pkgRepository) { - var namesToSave = Utils.ProcessNameWildcards(pkgNames, out string[] errorMsgs, out bool nameContainsWildcard); + var namesToSave = Utils.ProcessNameWildcards(pkgNames, removeWildcardEntries:false, out string[] errorMsgs, out bool nameContainsWildcard); if (nameContainsWildcard) { WriteError(new ErrorRecord( @@ -271,25 +273,26 @@ private void ProcessSaveHelper(string[] pkgNames, bool pkgPrerelease, string[] p } var installedPkgs = _installHelper.InstallPackages( - names: namesToSave, - versionRange: _versionRange, - prerelease: pkgPrerelease, - repository: pkgRepository, - acceptLicense: true, - quiet: Quiet, - reinstall: true, - force: false, + names: namesToSave, + versionRange: _versionRange, + versionString: Version, + prerelease: pkgPrerelease, + repository: pkgRepository, + acceptLicense: true, + quiet: Quiet, + reinstall: true, + force: false, trustRepository: TrustRepository, - credential: Credential, - noClobber: false, - asNupkg: AsNupkg, - includeXml: IncludeXml, + noClobber: false, + asNupkg: AsNupkg, + includeXml: IncludeXml, skipDependencyCheck: SkipDependencyCheck, authenticodeCheck: AuthenticodeCheck, savePkg: true, pathsToInstallPkg: new List { _path }, scope: null, - tmpPath: _tmpPath); + tmpPath: _tmpPath, + pkgsInstalled: new HashSet(StringComparer.InvariantCultureIgnoreCase)); if (PassThru) { diff --git a/src/code/ServerApiCall.cs b/src/code/ServerApiCall.cs new file mode 100644 index 000000000..31c141df5 --- /dev/null +++ b/src/code/ServerApiCall.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using System; +using System.Collections.Generic; +using System.Net.Http; +using NuGet.Versioning; +using System.Net; +using System.Runtime.ExceptionServices; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal abstract class ServerApiCall : IServerAPICalls + { + #region Members + + public abstract PSRepositoryInfo repository { get; set; } + public abstract HttpClient s_client { get; set; } + + #endregion + + #region Constructor + + public ServerApiCall(PSRepositoryInfo repository, NetworkCredential networkCredential) + { + this.repository = repository; + HttpClientHandler handler = new HttpClientHandler() + { + Credentials = networkCredential + }; + + s_client = new HttpClient(handler); + } + + #endregion + + #region Methods + // High level design: Find-PSResource >>> IFindPSResource (loops, version checks, etc.) >>> IServerAPICalls (call to repository endpoint/url) + + /// + /// Find method which allows for searching for all packages from a repository and returns latest version for each. + /// + public abstract string[] FindAll(bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for packages with tag from a repository and returns latest version for each. + /// Examples: Search -Tag "JSON" -Repository PSGallery + /// API call: + /// - Include prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsAbsoluteLatestVersion&searchTerm=tag:JSON&includePrerelease=true + /// + public abstract string[] FindTag(string tag, bool includePrerelease, ResourceType _type, out ExceptionDispatchInfo edi); + + public abstract string[] FindCommandOrDscResource(string tag, bool includePrerelease, bool isSearchingForCommands, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// Examples: Search "PowerShellGet" + /// API call: + /// - No prerelease: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// - Include prerelease: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// Implementation Note: Need to filter further for latest version (prerelease or non-prerelease dependening on user preference) + /// + public abstract string FindName(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + public abstract string FindNameWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for single name with wildcards and returns latest version. + /// Name: supports wildcards + /// Examples: Search "PowerShell*" + /// API call: + /// - No prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsLatestVersion&searchTerm='az*' + /// Implementation Note: filter additionally and verify ONLY package name was a match. + /// + public abstract string[] FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + + public abstract string[] FindNameGlobbingWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi); + /// + /// Find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// Examples: Search "PowerShellGet" "[3.0.0.0, 5.0.0.0]" + /// Search "PowerShellGet" "3.*" + /// API Call: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// Implementation note: Returns all versions, including prerelease ones. Later (in the API client side) we'll do filtering on the versions to satisfy what user provided. + /// + public abstract string[] FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ExceptionDispatchInfo edi); + + /// + /// Find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "PowerShellGet" "2.2.5" + /// API call: http://www.powershellgallery.com/api/v2/Packages(Id='PowerShellGet', Version='2.2.5') + /// + public abstract string FindVersion(string packageName, string version, ResourceType type, out ExceptionDispatchInfo edi); + + public abstract string FindVersionWithTag(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi); + + /** INSTALL APIS **/ + + /// + /// Installs specific package. + /// Name: no wildcard support. + /// Examples: Install "PowerShellGet" + /// Implementation Note: if not prerelease: https://www.powershellgallery.com/api/v2/package/powershellget (Returns latest stable) + /// if prerelease, the calling method should first call IFindPSResource.FindName(), + /// then find the exact version to install, then call into install version + /// + public abstract HttpContent InstallName(string packageName, bool includePrerelease, out ExceptionDispatchInfo edi); + + + /// + /// Installs package with specific name and version. + /// Name: no wildcard support. + /// Version: no wildcard support. + /// Examples: Install "PowerShellGet" -Version "3.0.0.0" + /// Install "PowerShellGet" -Version "3.0.0-beta16" + /// API Call: https://www.powershellgallery.com/api/v2/package/Id/version (version can be prerelease) + /// + public abstract HttpContent InstallVersion(string packageName, string version, out ExceptionDispatchInfo edi); + + #endregion + + } +} \ No newline at end of file diff --git a/src/code/ServerFactory.cs b/src/code/ServerFactory.cs new file mode 100644 index 000000000..119bad2d6 --- /dev/null +++ b/src/code/ServerFactory.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using System.Net; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal class ServerFactory + { + public static ServerApiCall GetServer(PSRepositoryInfo repository, NetworkCredential networkCredential) + { + PSRepositoryInfo.APIVersion repoApiVersion = repository.ApiVersion; + ServerApiCall currentServer = null; + + switch (repoApiVersion) + { + case PSRepositoryInfo.APIVersion.v2: + currentServer = new V2ServerAPICalls(repository, networkCredential); + break; + + case PSRepositoryInfo.APIVersion.v3: + currentServer = new V3ServerAPICalls(repository, networkCredential); + break; + } + + return currentServer; + } + } +} \ No newline at end of file diff --git a/src/code/UninstallPSResource.cs b/src/code/UninstallPSResource.cs index c2fad5d08..971aac6e7 100644 --- a/src/code/UninstallPSResource.cs +++ b/src/code/UninstallPSResource.cs @@ -101,7 +101,7 @@ protected override void ProcessRecord() ThrowTerminatingError(IncorrectVersionFormat); } - Name = Utils.ProcessNameWildcards(Name, out string[] errorMsgs, out bool _); + Name = Utils.ProcessNameWildcards(Name, removeWildcardEntries:false, out string[] errorMsgs, out bool _); foreach (string error in errorMsgs) { diff --git a/src/code/UnregisterPSResourceRepository.cs b/src/code/UnregisterPSResourceRepository.cs index c18257007..058393c1d 100644 --- a/src/code/UnregisterPSResourceRepository.cs +++ b/src/code/UnregisterPSResourceRepository.cs @@ -45,7 +45,7 @@ protected override void BeginProcessing() } protected override void ProcessRecord() { - Name = Utils.ProcessNameWildcards(Name, out string[] _, out bool nameContainsWildcard); + Name = Utils.ProcessNameWildcards(Name, removeWildcardEntries:false, out string[] _, out bool nameContainsWildcard); if (nameContainsWildcard) { var message = String.Format("Name: '{0}, cannot contain wildcards", String.Join(", ", Name)); diff --git a/src/code/UpdatePSResource.cs b/src/code/UpdatePSResource.cs index 63bc7ea07..0dd2846cf 100644 --- a/src/code/UpdatePSResource.cs +++ b/src/code/UpdatePSResource.cs @@ -5,9 +5,9 @@ using NuGet.Versioning; using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Management.Automation; +using System.Net; using System.Threading; namespace Microsoft.PowerShell.PowerShellGet.Cmdlets @@ -155,35 +155,54 @@ protected override void BeginProcessing() RepositorySettings.CheckRepositoryStore(); _pathsToInstallPkg = Utils.GetAllInstallationPaths(this, Scope); - _cancellationTokenSource = new CancellationTokenSource(); + var networkCred = Credential != null ? new NetworkCredential(Credential.UserName, Credential.Password) : null; + _findHelper = new FindHelper( cancellationToken: _cancellationTokenSource.Token, - cmdletPassedIn: this); + cmdletPassedIn: this, + networkCredential: networkCred); - _installHelper = new InstallHelper(cmdletPassedIn: this); + _installHelper = new InstallHelper(cmdletPassedIn: this, networkCredential: networkCred); } protected override void ProcessRecord() { - VersionRange versionRange; + // determine/parse out Version param + VersionType versionType = VersionType.VersionRange; + NuGetVersion nugetVersion = null; + VersionRange versionRange = null; - // handle case where Version == null - if (Version == null) { - versionRange = VersionRange.All; + if (Version != null) + { + if (!NuGetVersion.TryParse(Version, out nugetVersion)) + { + if (Version.Trim().Equals("*")) + { + versionRange = VersionRange.All; + versionType = VersionType.VersionRange; + } + else if (!VersionRange.TryParse(Version, out versionRange)) + { + WriteError(new ErrorRecord( + new ArgumentException("Argument for -Version parameter is not in the proper format"), + "IncorrectVersionFormat", + ErrorCategory.InvalidArgument, + this)); + return; + } + } + else + { + versionType = VersionType.SpecificVersion; + } } - else if (!Utils.TryParseVersionOrVersionRange(Version, out versionRange)) + else { - // Only returns false if the range was incorrectly formatted and couldn't be parsed. - WriteError(new ErrorRecord( - new PSInvalidOperationException("Cannot parse Version parameter provided into VersionRange"), - "ErrorParsingVersionParamIntoVersionRange", - ErrorCategory.InvalidArgument, - this)); - return; + versionType = VersionType.NoVersion; } - var namesToUpdate = ProcessPackageNames(Name, versionRange); + var namesToUpdate = ProcessPackageNames(Name, versionRange, nugetVersion, versionType); if (namesToUpdate.Length == 0) { @@ -199,6 +218,7 @@ protected override void ProcessRecord() var installedPkgs = _installHelper.InstallPackages( names: namesToUpdate, versionRange: versionRange, + versionString: Version, prerelease: Prerelease, repository: Repository, acceptLicense: AcceptLicense, @@ -206,7 +226,6 @@ protected override void ProcessRecord() reinstall: true, force: Force, trustRepository: TrustRepository, - credential: Credential, noClobber: false, asNupkg: false, includeXml: true, @@ -215,7 +234,8 @@ protected override void ProcessRecord() savePkg: false, pathsToInstallPkg: _pathsToInstallPkg, scope: Scope, - tmpPath: _tmpPath); + tmpPath: _tmpPath, + pkgsInstalled: new HashSet(StringComparer.InvariantCultureIgnoreCase)); if (PassThru) { @@ -252,10 +272,13 @@ protected override void EndProcessing() /// private string[] ProcessPackageNames( string[] namesToProcess, - VersionRange versionRange) + VersionRange versionRange, + NuGetVersion nuGetVersion, + VersionType versionType) { namesToProcess = Utils.ProcessNameWildcards( pkgNames: namesToProcess, + removeWildcardEntries:false, errorMsgs: out string[] errorMsgs, isContainWildcard: out bool _); @@ -315,11 +338,13 @@ private string[] ProcessPackageNames( foreach (var foundResource in _findHelper.FindByResourceName( name: installedPackages.Keys.ToArray(), type: ResourceType.None, + versionRange: versionRange, + nugetVersion: nuGetVersion, + versionType: versionType, version: Version, prerelease: Prerelease, tag: null, repository: Repository, - credential: Credential, includeDependencies: !SkipDependencyCheck)) { if (!repositoryPackages.ContainsKey(foundResource.Name)) @@ -362,22 +387,14 @@ private string[] ProcessPackageNames( continue; } - if (!NuGetVersion.TryParse(repositoryPackage.Version.ToString(), out NuGetVersion repositoryPackageNuGetVersion)) - { - WriteWarning($"Cannot parse nuget version in repository package '{repositoryPackage.Name}'. Cannot update package."); - continue; - } - - // We compare NuGetVersions instead of System.Version as repositoryPackage.Version (3.0.17.0) and installedPackage.Version (3.0.17.-1) - // should refer to the same version but with System.Version end up having discrepancies which yields incorrect results. - if ((versionRange == VersionRange.All && repositoryPackageNuGetVersion > installedVersion) || - !versionRange.Satisfies(installedVersion)) + if (((versionRange == null || versionRange == VersionRange.All) && repositoryPackage.Version > installedPackage.Version) || + (versionRange != null && !versionRange.Satisfies(installedVersion))) { namesToUpdate.Add(repositoryPackage.Name); } else { - WriteVerbose($"Installed package {repositoryPackage.Name} {repositoryPackageNuGetVersion} is already up to date."); + WriteVerbose($"Installed package {repositoryPackage.Name} {repositoryPackage.Version} is already up to date."); } } diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 77ad2d313..90f2723a1 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Microsoft.Win32.SafeHandles; using NuGet.Versioning; using System; using System.Collections; @@ -9,14 +8,12 @@ using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Net; using System.Management.Automation; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Runtime.InteropServices; -using System.Security; -using System.Security.Cryptography.X509Certificates; using Microsoft.PowerShell.Commands; +using Microsoft.PowerShell.PowerShellGet.Cmdlets; namespace Microsoft.PowerShell.PowerShellGet.UtilClasses { @@ -127,6 +124,7 @@ public static string[] GetStringArray(ArrayList list) public static string[] ProcessNameWildcards( string[] pkgNames, + bool removeWildcardEntries, out string[] errorMsgs, out bool isContainWildcard) { @@ -145,6 +143,13 @@ public static string[] ProcessNameWildcards( { if (WildcardPattern.ContainsWildcardCharacters(name)) { + if (removeWildcardEntries) + { + // Tag // CommandName // DSCResourceName + errorMsgsList.Add($"{name} will be discarded from the provided entries."); + continue; + } + if (String.Equals(name, "*", StringComparison.InvariantCultureIgnoreCase)) { isContainWildcard = true; @@ -234,7 +239,6 @@ public static bool TryParseVersionOrVersionRange( return true; } - // parse as Version range return VersionRange.TryParse(version, out versionRange); } @@ -267,11 +271,10 @@ public static bool GetVersionForInstallPath( return false; } - string version = psGetInfo.Version.ToString(); - string prerelease = psGetInfo.Prerelease; + psGetInfo.AdditionalMetadata.TryGetValue("NormalizedVersion", out string normalizedVersion); if (!NuGetVersion.TryParse( - value: String.IsNullOrEmpty(prerelease) ? version : GetNormalizedVersionString(version, prerelease), + value: normalizedVersion, version: out pkgNuGetVersion)) { cmdletPassedIn.WriteVerbose(String.Format("Leaf directory in path '{0}' cannot be parsed into a version.", installedPkgPath)); @@ -732,6 +735,45 @@ private static void GetStandardPlatformPaths( } } + /// + /// Checks if any of the package versions are already installed and if they are removes them from the list of packages to install. + /// + internal static HashSet GetInstalledPackages(List pathsToSearch, PSCmdlet cmdletPassedIn) + { + // Package install paths. + // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable). + // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations. + // e.g.: + // ./InstallPackagePath1/PackageA + // ./InstallPackagePath1/PackageB + // ./InstallPackagePath2/PackageC + // ./InstallPackagePath3/PackageD + + // Get currently installed packages. + var getHelper = new GetHelper(cmdletPassedIn); + var pkgsInstalledOnMachine = new HashSet(StringComparer.CurrentCultureIgnoreCase); + + foreach (PSResourceInfo installedPkg in getHelper.GetPackagesFromPath( + name: new string[] { "*" }, + versionRange: VersionRange.All, + pathsToSearch: pathsToSearch, + selectPrereleaseOnly: false)) + { + string pkgNameVersion = CreateHashSetKey(installedPkg.Name, installedPkg.Version.ToString()); + if (!pkgsInstalledOnMachine.Contains(pkgNameVersion)) + { + pkgsInstalledOnMachine.Add(pkgNameVersion); + } + } + + return pkgsInstalledOnMachine; + } + + internal static string CreateHashSetKey(string packageName, string packageVersion) + { + return $"{packageName}{packageVersion}"; + } + #endregion #region PSDataFile parsing @@ -1186,6 +1228,7 @@ private static void RestoreDirContents( } #endregion + } #endregion @@ -1364,6 +1407,7 @@ internal static bool CheckAuthenticodeSignature( return true; } + /* // First check if the files are catalog signed. string catalogFilePath = Path.Combine(tempDirNameVersion, pkgName + ".cat"); if (File.Exists(catalogFilePath)) @@ -1413,6 +1457,7 @@ internal static bool CheckAuthenticodeSignature( return true; } + */ // Otherwise check for signatures on individual files. Collection authenticodeSignatures; diff --git a/src/code/V2ResponseUtil.cs b/src/code/V2ResponseUtil.cs new file mode 100644 index 000000000..f32d9c2c8 --- /dev/null +++ b/src/code/V2ResponseUtil.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Xml; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal class V2ResponseUtil : ResponseUtil + { + #region Members + + public override PSRepositoryInfo repository { get; set; } + + #endregion + + #region Constructor + + public V2ResponseUtil(PSRepositoryInfo repository) : base(repository) + { + this.repository = repository; + } + + #endregion + + #region Overriden Methods + public override IEnumerable ConvertToPSResourceResult(string[] responses) + { + // in FindHelper: + // serverApi.FindName() -> return responses, and out errRecord + // check outErrorRecord + // + // v2Converter.ConvertToPSResourceInfo(responses) -> return PSResourceResult + // check resourceResult for error, write if needed + + foreach (string response in responses) + { + var elemList = ConvertResponseToXML(response); + if (elemList.Length == 0) + { + // this indicates we got a non-empty, XML response (as noticed for V2 server) but it's not a response that's meaningful (contains 'properties') + string errorMsg = $"Response didn't contain properties element"; + yield return new PSResourceResult(returnedObject: null, errorMsg: errorMsg, isTerminatingError: false); + } + + foreach (var element in elemList) + { + if (!PSResourceInfo.TryConvertFromXml(element, out PSResourceInfo psGetInfo, repository.Name, out string errorMsg)) + { + yield return new PSResourceResult(returnedObject: null, errorMsg: errorMsg, isTerminatingError: false); + } + + yield return new PSResourceResult(returnedObject: psGetInfo, errorMsg: String.Empty, isTerminatingError: false); + } + } + } + + #endregion + + #region V2 Specific Methods + + public XmlNode[] ConvertResponseToXML(string httpResponse) { + + //Create the XmlDocument. + XmlDocument doc = new XmlDocument(); + doc.LoadXml(httpResponse); + + XmlNodeList elemList = doc.GetElementsByTagName("m:properties"); + + XmlNode[] nodes = new XmlNode[elemList.Count]; + for (int i=0; i>> IFindPSResource (loops, version checks, etc.) >>> IServerAPICalls (call to repository endpoint/url) + + /// + /// Find method which allows for searching for all packages from a repository and returns latest version for each. + /// Examples: Search -Repository PSGallery + /// API call: + /// - No prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsLatestVersion + /// + public override string[] FindAll(bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) { + edi = null; + List responses = new List(); + + if (type == ResourceType.Script || type == ResourceType.None) + { + int scriptSkip = 0; + string initialScriptResponse = FindAllFromTypeEndPoint(includePrerelease, isSearchingModule: false, scriptSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(initialScriptResponse); + int initalScriptCount = GetCountFromResponse(initialScriptResponse, out edi); + if (edi != null) + { + return responses.ToArray(); + } + int count = initalScriptCount / 6000; + // if more than 100 count, loop and add response to list + while (count > 0) + { + scriptSkip += 6000; + var tmpResponse = FindAllFromTypeEndPoint(includePrerelease, isSearchingModule: false, scriptSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + } + if (type != ResourceType.Script) + { + int moduleSkip = 0; + string initialModuleResponse = FindAllFromTypeEndPoint(includePrerelease, isSearchingModule: true, moduleSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(initialModuleResponse); + int initalModuleCount = GetCountFromResponse(initialModuleResponse, out edi); + if (edi != null) + { + return responses.ToArray(); + } + int count = initalModuleCount / 6000; + + // if more than 100 count, loop and add response to list + while (count > 0) + { + moduleSkip += 6000; + var tmpResponse = FindAllFromTypeEndPoint(includePrerelease, isSearchingModule: true, moduleSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + } + + return responses.ToArray(); + } + + /// + /// Find method which allows for searching for packages with tag from a repository and returns latest version for each. + /// Examples: Search -Tag "JSON" -Repository PSGallery + /// API call: + /// - Include prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsAbsoluteLatestVersion&searchTerm=tag:JSON&includePrerelease=true + /// + public override string[] FindTag(string tag, bool includePrerelease, ResourceType _type, out ExceptionDispatchInfo edi) + { + edi = null; + List responses = new List(); + + if (_type == ResourceType.Script || _type == ResourceType.None) + { + int scriptSkip = 0; + string initialScriptResponse = FindTagFromEndpoint(tag, includePrerelease, isSearchingModule: false, scriptSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(initialScriptResponse); + int initalScriptCount = GetCountFromResponse(initialScriptResponse, out edi); + if (edi != null) + { + return responses.ToArray(); + } + int count = initalScriptCount / 100; + // if more than 100 count, loop and add response to list + while (count > 0) + { + // skip 100 + scriptSkip += 100; + var tmpResponse = FindTagFromEndpoint(tag, includePrerelease, isSearchingModule: false, scriptSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + } + if (_type != ResourceType.Script) + { + int moduleSkip = 0; + string initialModuleResponse = FindTagFromEndpoint(tag, includePrerelease, isSearchingModule: true, moduleSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(initialModuleResponse); + int initalModuleCount = GetCountFromResponse(initialModuleResponse, out edi); + if (edi != null) + { + return responses.ToArray(); + } + int count = initalModuleCount / 100; + // if more than 100 count, loop and add response to list + while (count > 0) + { + moduleSkip += 100; + var tmpResponse = FindTagFromEndpoint(tag, includePrerelease, isSearchingModule: true, moduleSkip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + } + + return responses.ToArray(); + } + + public override string[] FindCommandOrDscResource(string tag, bool includePrerelease, bool isSearchingForCommands, out ExceptionDispatchInfo edi) + { + List responses = new List(); + int skip = 0; + + string initialResponse = FindCommandOrDscResource(tag, includePrerelease, isSearchingForCommands, skip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(initialResponse); + int initialCount = GetCountFromResponse(initialResponse, out edi); + if (edi != null) + { + return responses.ToArray(); + } + int count = initialCount / 100; + + while (count > 0) + { + skip += 100; + var tmpResponse = FindCommandOrDscResource(tag, includePrerelease, isSearchingForCommands, skip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + + return responses.ToArray(); + } + + /// + /// Find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// Examples: Search "PowerShellGet" + /// API call: + /// - No prerelease: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// - Include prerelease: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// Implementation Note: Need to filter further for latest version (prerelease or non-prerelease dependening on user preference) + /// + public override string FindName(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + // Make sure to include quotations around the package name + var prerelease = includePrerelease ? "IsAbsoluteLatestVersion" : "IsLatestVersion"; + + // This should return the latest stable version or the latest prerelease version (respectively) + // https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet'&$filter=IsLatestVersion and substringof('PSModule', Tags) eq true + string typeFilterPart = type == ResourceType.None ? $" and Id eq '{packageName}'" : $" and substringof('PS{type.ToString()}', Tags) eq true"; + var requestUrlV2 = $"{repository.Uri}/FindPackagesById()?id='{packageName}'&$filter={prerelease}{typeFilterPart}&{select}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + public override string FindNameWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + // Make sure to include quotations around the package name + var prerelease = includePrerelease ? "IsAbsoluteLatestVersion" : "IsLatestVersion"; + + // This should return the latest stable version or the latest prerelease version (respectively) + // https://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet'&$filter=IsLatestVersion and substringof('PSModule', Tags) eq true + string typeFilterPart = type == ResourceType.None ? $" and Id eq '{packageName}'" : $" and substringof('PS{type.ToString()}', Tags) eq true"; + + string tagFilterPart = String.Empty; + foreach (string tag in tags) + { + tagFilterPart += $" and substringof('{tag}', Tags) eq true"; + } + + var requestUrlV2 = $"{repository.Uri}/FindPackagesById()?id='{packageName}'&$filter={prerelease}{typeFilterPart}{tagFilterPart}&{select}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + /// + /// Find method which allows for searching for single name with wildcards and returns latest version. + /// Name: supports wildcards + /// Examples: Search "PowerShell*" + /// API call: + /// - No prerelease: http://www.powershellgallery.com/api/v2/Search()?$filter=IsLatestVersion&searchTerm='az*' + /// Implementation Note: filter additionally and verify ONLY package name was a match. + /// + public override string[] FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + List responses = new List(); + int skip = 0; + + var initialResponse = FindNameGlobbing(packageName, type, includePrerelease, skip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + + responses.Add(initialResponse); + + // check count (regex) 425 ==> count/100 ~~> 4 calls + int initalCount = GetCountFromResponse(initialResponse, out edi); // count = 4 + if (edi != null) + { + return responses.ToArray(); + } + int count = initalCount / 100; + // if more than 100 count, loop and add response to list + while (count > 0) + { + // skip 100 + skip += 100; + var tmpResponse = FindNameGlobbing(packageName, type, includePrerelease, skip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + + return responses.ToArray(); + } + + public override string[] FindNameGlobbingWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + List responses = new List(); + int skip = 0; + + var initialResponse = FindNameGlobbingWithTag(packageName, tags, type, includePrerelease, skip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + + responses.Add(initialResponse); + + // check count (regex) 425 ==> count/100 ~~> 4 calls + int initalCount = GetCountFromResponse(initialResponse, out edi); // count = 4 + if (edi != null) + { + return responses.ToArray(); + } + int count = initalCount / 100; + // if more than 100 count, loop and add response to list + while (count > 0) + { + // skip 100 + skip += 100; + var tmpResponse = FindNameGlobbingWithTag(packageName, tags, type, includePrerelease, skip, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + + return responses.ToArray(); + } + + /// + /// Find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// Examples: Search "PowerShellGet" "[3.0.0.0, 5.0.0.0]" + /// Search "PowerShellGet" "3.*" + /// API Call: http://www.powershellgallery.com/api/v2/FindPackagesById()?id='PowerShellGet' + /// Implementation note: Returns all versions, including prerelease ones. Later (in the API client side) we'll do filtering on the versions to satisfy what user provided. + /// + public override string[] FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ExceptionDispatchInfo edi) + { + List responses = new List(); + int skip = 0; + + var initialResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, skip, getOnlyLatest, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(initialResponse); + + if (!getOnlyLatest) + { + int initalCount = GetCountFromResponse(initialResponse, out edi); + if (edi != null) + { + return responses.ToArray(); + } + int count = initalCount / 100; + + while (count > 0) + { + // skip 100 + skip += 100; + var tmpResponse = FindVersionGlobbing(packageName, versionRange, includePrerelease, type, skip, getOnlyLatest, out edi); + if (edi != null) + { + return responses.ToArray(); + } + responses.Add(tmpResponse); + count--; + } + } + + return responses.ToArray(); + } + + /// + /// Find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "PowerShellGet" "2.2.5" + /// API call: http://www.powershellgallery.com/api/v2/Packages(Id='PowerShellGet', Version='2.2.5') + /// + public override string FindVersion(string packageName, string version, ResourceType type, out ExceptionDispatchInfo edi) + { + // https://www.powershellgallery.com/api/v2//FindPackagesById()?id='blah'&includePrerelease=false&$filter= NormalizedVersion eq '1.1.0' and substringof('PSModule', Tags) eq true + // Quotations around package name and version do not matter, same metadata gets returned. + string typeFilterPart = type == ResourceType.None ? String.Empty : $" and substringof('PS{type.ToString()}', Tags) eq true"; + var requestUrlV2 = $"{repository.Uri}/FindPackagesById()?id='{packageName}'&$filter= NormalizedVersion eq '{version}'{typeFilterPart}&{select}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + public override string FindVersionWithTag(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi) + { + string typeFilterPart = type == ResourceType.None ? String.Empty : $" and substringof('PS{type.ToString()}', Tags) eq true"; + + string tagFilterPart = String.Empty; + foreach (string tag in tags) + { + tagFilterPart += $" and substringof('{tag}', Tags) eq true"; + } + + var requestUrlV2 = $"{repository.Uri}/FindPackagesById()?id='{packageName}'&$filter= NormalizedVersion eq '{version}'{typeFilterPart}{tagFilterPart}&{select}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + + /** INSTALL APIS **/ + + /// + /// Installs specific package. + /// Name: no wildcard support. + /// Examples: Install "PowerShellGet" + /// Implementation Note: if not prerelease: https://www.powershellgallery.com/api/v2/package/powershellget (Returns latest stable) + /// if prerelease, call into InstallVersion instead. + /// + public override HttpContent InstallName(string packageName, bool includePrerelease, out ExceptionDispatchInfo edi) + { + var requestUrlV2 = $"{repository.Uri}/package/{packageName}"; + + return HttpRequestCallForContent(requestUrlV2, out edi); + } + + /// + /// Installs package with specific name and version. + /// Name: no wildcard support. + /// Version: no wildcard support. + /// Examples: Install "PowerShellGet" -Version "3.0.0.0" + /// Install "PowerShellGet" -Version "3.0.0-beta16" + /// API Call: https://www.powershellgallery.com/api/v2/package/Id/version (version can be prerelease) + /// + public override HttpContent InstallVersion(string packageName, string version, out ExceptionDispatchInfo edi) + { + var requestUrlV2 = $"{repository.Uri}/package/{packageName}/{version}"; + + return HttpRequestCallForContent(requestUrlV2, out edi); + } + + + private string HttpRequestCall(string requestUrlV2, out ExceptionDispatchInfo edi) + { + edi = null; + string response = string.Empty; + + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV2); + + response = SendV2RequestAsync(request, s_client).GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (ArgumentNullException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (InvalidOperationException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + + return response; + } + + private HttpContent HttpRequestCallForContent(string requestUrlV2, out ExceptionDispatchInfo edi) + { + edi = null; + HttpContent content = null; + + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV2); + + content = SendV2RequestForContentAsync(request, s_client).GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (ArgumentNullException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (InvalidOperationException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + + return content; + } + + + #endregion + + #region Private Methods + + /// + /// Helper method for string[] FindAll(string, PSRepositoryInfo, bool, bool, ResourceType, out string) + /// + private string FindAllFromTypeEndPoint(bool includePrerelease, bool isSearchingModule, int skip, out ExceptionDispatchInfo edi) + { + string typeEndpoint = isSearchingModule ? String.Empty : "/items/psscript"; + string paginationParam = $"&$orderby=Id desc&$inlinecount=allpages&$skip={skip}&$top=6000"; + var prereleaseFilter = includePrerelease ? "IsAbsoluteLatestVersion&includePrerelease=true" : "IsLatestVersion"; + + var requestUrlV2 = $"{repository.Uri}{typeEndpoint}/Search()?$filter={prereleaseFilter}{paginationParam}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + /// + /// Helper method for string[] FindTag(string, PSRepositoryInfo, bool, bool, ResourceType, out string) + /// + private string FindTagFromEndpoint(string tag, bool includePrerelease, bool isSearchingModule, int skip, out ExceptionDispatchInfo edi) + { + // scenarios with type + tags: + // type: None -> search both endpoints + // type: M -> just search Module endpoint + // type: S -> just search Scripts end point + // type: DSCResource -> just search Modules + // type: Command -> just search Modules + string typeEndpoint = isSearchingModule ? String.Empty : "/items/psscript"; + string paginationParam = $"&$orderby=Id desc&$inlinecount=allpages&$skip={skip}&$top=6000"; + var prereleaseFilter = includePrerelease ? "$filter=IsAbsoluteLatestVersion&includePrerelease=true" : "$filter=IsLatestVersion"; + + var scriptsRequestUrlV2 = $"{repository.Uri}{typeEndpoint}/Search()?{prereleaseFilter}&searchTerm='tag:{tag}'{paginationParam}&{select}"; + + return HttpRequestCall(requestUrlV2: scriptsRequestUrlV2, out edi); + } + + /// + /// Helper method for string[] FindCommandOrDSCResource(string, PSRepositoryInfo, bool, bool, ResourceType, out string) + /// + private string FindCommandOrDscResource(string tag, bool includePrerelease, bool isSearchingForCommands, int skip, out ExceptionDispatchInfo edi) + { + // can only find from Modules endpoint + string paginationParam = $"&$orderby=Id desc&$inlinecount=allpages&$skip={skip}&$top=6000"; + var prereleaseFilter = includePrerelease ? "$filter=IsAbsoluteLatestVersion&includePrerelease=true" : "$filter=IsLatestVersion"; + var tagFilter = isSearchingForCommands ? "PSCommand_" : "PSDscResource_"; + var requestUrlV2 = $"{repository.Uri}/Search()?{prereleaseFilter}&searchTerm='tag:{tagFilter}{tag}'{prereleaseFilter}{paginationParam}&{select}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + /// + /// Helper method for string[] FindNameGlobbing() + /// + private string FindNameGlobbing(string packageName, ResourceType type, bool includePrerelease, int skip, out ExceptionDispatchInfo edi) + { + // https://www.powershellgallery.com/api/v2/Search()?$filter=endswith(Id, 'Get') and startswith(Id, 'PowerShell') and IsLatestVersion (stable) + // https://www.powershellgallery.com/api/v2/Search()?$filter=endswith(Id, 'Get') and IsAbsoluteLatestVersion&includePrerelease=true + + string extraParam = $"&$orderby=Id desc&$inlinecount=allpages&$skip={skip}&$top=100"; + var prerelease = includePrerelease ? "IsAbsoluteLatestVersion&includePrerelease=true" : "IsLatestVersion"; + string nameFilter; + + var names = packageName.Split(new char[] {'*'}, StringSplitOptions.RemoveEmptyEntries); + + if (names.Length == 0) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name '*' for V2 server protocol repositories is not supported")); + return string.Empty; + } + if (names.Length == 1) + { + if (packageName.StartsWith("*") && packageName.EndsWith("*")) + { + // *get* + nameFilter = $"substringof('{names[0]}', Id)"; + } + else if (packageName.EndsWith("*")) + { + // PowerShell* + nameFilter = $"startswith(Id, '{names[0]}')"; + } + else + { + // *ShellGet + nameFilter = $"endswith(Id, '{names[0]}')"; + } + } + else if (names.Length == 2 && !packageName.StartsWith("*") && !packageName.EndsWith("*")) + { + // *pow*get* + // pow*get -> only support this + // pow*get* + // *pow*get + nameFilter = $"startswith(Id, '{names[0]}') and endswith(Id, '{names[1]}')"; + } + else + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name with wildcards is only supported for scenarios similar to the following examples: PowerShell*, *ShellGet, *Shell*.")); + return string.Empty; + } + + string typeFilterPart = type == ResourceType.None ? String.Empty : $" and substringof('PS{type.ToString()}', Tags) eq true"; + + var requestUrlV2 = $"{repository.Uri}/Search()?$filter={nameFilter}{typeFilterPart} and {prerelease}&{select}{extraParam}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + private string FindNameGlobbingWithTag(string packageName, string[] tags, ResourceType type, bool includePrerelease, int skip, out ExceptionDispatchInfo edi) + { + // https://www.powershellgallery.com/api/v2/Search()?$filter=endswith(Id, 'Get') and startswith(Id, 'PowerShell') and IsLatestVersion (stable) + // https://www.powershellgallery.com/api/v2/Search()?$filter=endswith(Id, 'Get') and IsAbsoluteLatestVersion&includePrerelease=true + + string extraParam = $"&$orderby=Id desc&$inlinecount=allpages&$skip={skip}&$top=100"; + var prerelease = includePrerelease ? "IsAbsoluteLatestVersion&includePrerelease=true" : "IsLatestVersion"; + string nameFilter; + + var names = packageName.Split(new char[] {'*'}, StringSplitOptions.RemoveEmptyEntries); + + if (names.Length == 0) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name '*' for V2 server protocol repositories is not supported")); + return string.Empty; + } + if (names.Length == 1) + { + if (packageName.StartsWith("*") && packageName.EndsWith("*")) + { + // *get* + nameFilter = $"substringof('{names[0]}', Id)"; + } + else if (packageName.EndsWith("*")) + { + // PowerShell* + nameFilter = $"startswith(Id, '{names[0]}')"; + } + else + { + // *ShellGet + nameFilter = $"endswith(Id, '{names[0]}')"; + } + } + else if (names.Length == 2 && !packageName.StartsWith("*") && !packageName.EndsWith("*")) + { + // *pow*get* + // pow*get -> only support this + // pow*get* + // *pow*get + nameFilter = $"startswith(Id, '{names[0]}') and endswith(Id, '{names[1]}')"; + } + else + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name with wildcards is only supported for scenarios similar to the following examples: PowerShell*, *ShellGet, *Shell*.")); + return string.Empty; + } + + string tagFilterPart = String.Empty; + foreach (string tag in tags) + { + tagFilterPart += $" and substringof('{tag}', Tags) eq true"; + } + + string typeFilterPart = type == ResourceType.None ? String.Empty : $" and substringof('PS{type.ToString()}', Tags) eq true"; + var requestUrlV2 = $"{repository.Uri}/Search()?$filter={nameFilter}{tagFilterPart}{typeFilterPart} and {prerelease}&{select}{extraParam}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + /// + /// Helper method for string[] FindVersionGlobbing() + /// + private string FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, int skip, bool getOnlyLatest, out ExceptionDispatchInfo edi) + { + //https://www.powershellgallery.com/api/v2//FindPackagesById()?id='blah'&includePrerelease=false&$filter= NormalizedVersion gt '1.0.0' and NormalizedVersion lt '2.2.5' and substringof('PSModule', Tags) eq true + //https://www.powershellgallery.com/api/v2//FindPackagesById()?id='PowerShellGet'&includePrerelease=false&$filter= NormalizedVersion gt '1.1.1' and NormalizedVersion lt '2.2.5' + // NormalizedVersion doesn't include trailing zeroes + // Notes: this could allow us to take a version range (i.e (2.0.0, 3.0.0.0]) and deconstruct it and add options to the Filter for Version to describe that range + // will need to filter additionally, if IncludePrerelease=false, by default we get stable + prerelease both back + // Current bug: Find PSGet -Version "2.0.*" -> https://www.powershellgallery.com/api/v2//FindPackagesById()?id='PowerShellGet'&includePrerelease=false&$filter= Version gt '2.0.*' and Version lt '2.1' + // Make sure to include quotations around the package name + + //and IsPrerelease eq false + // ex: + // (2.0.0, 3.0.0) + // $filter= NVersion gt '2.0.0' and NVersion lt '3.0.0' + + // [2.0.0, 3.0.0] + // $filter= NVersion ge '2.0.0' and NVersion le '3.0.0' + + // [2.0.0, 3.0.0) + // $filter= NVersion ge '2.0.0' and NVersion lt '3.0.0' + + // (2.0.0, 3.0.0] + // $filter= NVersion gt '2.0.0' and NVersion le '3.0.0' + + // [, 2.0.0] + // $filter= NVersion le '2.0.0' + + string format = "NormalizedVersion {0} {1}"; + string minPart = String.Empty; + string maxPart = String.Empty; + + if (versionRange.MinVersion != null) + { + string operation = versionRange.IsMinInclusive ? "ge" : "gt"; + minPart = String.Format(format, operation, $"'{versionRange.MinVersion.ToNormalizedString()}'"); + } + + if (versionRange.MaxVersion != null) + { + string operation = versionRange.IsMaxInclusive ? "le" : "lt"; + maxPart = String.Format(format, operation, $"'{versionRange.MaxVersion.ToNormalizedString()}'"); + } + + string versionFilterParts = String.Empty; + if (!String.IsNullOrEmpty(minPart) && !String.IsNullOrEmpty(maxPart)) + { + versionFilterParts += minPart + " and " + maxPart; + } + else if (!String.IsNullOrEmpty(minPart)) + { + versionFilterParts += minPart; + } + else if (!String.IsNullOrEmpty(maxPart)) + { + versionFilterParts += maxPart; + } + + string filterQuery = "&$filter="; + filterQuery += includePrerelease ? string.Empty : "IsPrerelease eq false"; + //filterQuery += type == ResourceType.None ? String.Empty : $" and substringof('PS{type.ToString()}', Tags) eq true"; + + string joiningOperator = filterQuery.EndsWith("=") ? String.Empty : " and " ; + filterQuery += type == ResourceType.None ? String.Empty : $"{joiningOperator}substringof('PS{type.ToString()}', Tags) eq true"; + + if (!String.IsNullOrEmpty(versionFilterParts)) + { + // Check if includePrerelease is true, if it is we want to add "$filter" + // Single case where version is "*" (or "[,]") and includePrerelease is true, then we do not want to add "$filter" to the requestUrl. + + // Note: could be null/empty if Version was "*" -> [,] + joiningOperator = filterQuery.EndsWith("=") ? String.Empty : " and " ; + filterQuery += $"{joiningOperator}{versionFilterParts}"; + } + + string topParam = getOnlyLatest ? "$top=1" : "$top=100"; // only need 1 package if interested in latest + string paginationParam = $"$inlinecount=allpages&$skip={skip}&{topParam}"; + + filterQuery = filterQuery.EndsWith("=") ? string.Empty : filterQuery; + var requestUrlV2 = $"{repository.Uri}/FindPackagesById()?id='{packageName}'&$orderby=NormalizedVersion desc&{paginationParam}&{select}{filterQuery}"; + + return HttpRequestCall(requestUrlV2, out edi); + } + + public int GetCountFromResponse(string httpResponse, out ExceptionDispatchInfo edi) + { + edi = null; + int count = 0; + + //Create the XmlDocument. + XmlDocument doc = new XmlDocument(); + + try + { + doc.LoadXml(httpResponse); + } + catch (XmlException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + if (edi != null) + { + return count; + } + + XmlNodeList elemList = doc.GetElementsByTagName("m:count"); + if (elemList.Count > 0) + { + XmlNode node = elemList[0]; + count = int.Parse(node.InnerText); + } + + return count; + } + + public static async Task SendV2RequestAsync(HttpRequestMessage message, HttpClient s_client) + { + string errMsg = "Error occured while trying to retrieve response: "; + try + { + HttpResponseMessage response = await s_client.SendAsync(message); + response.EnsureSuccessStatusCode(); + return response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + throw new HttpRequestException(errMsg + e.Message); + } + catch (ArgumentNullException e) + { + throw new ArgumentNullException(errMsg + e.Message); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException(errMsg + e.Message); + } + } + + public static async Task SendV2RequestForContentAsync(HttpRequestMessage message, HttpClient s_client) + { + string errMsg = "Error occured while trying to retrieve response for content: "; + try + { + HttpResponseMessage response = await s_client.SendAsync(message); + response.EnsureSuccessStatusCode(); + return response.Content; + } + catch (HttpRequestException e) + { + throw new HttpRequestException(errMsg + e.Message); + } + catch (ArgumentNullException e) + { + throw new ArgumentNullException(errMsg + e.Message); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException(errMsg + e.Message); + } + } + + #endregion + } +} diff --git a/src/code/V3ResponseUtil.cs b/src/code/V3ResponseUtil.cs new file mode 100644 index 000000000..e28490e46 --- /dev/null +++ b/src/code/V3ResponseUtil.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal class V3ResponseUtil : ResponseUtil + { + #region Members + + public override PSRepositoryInfo repository { get; set; } + + #endregion + + #region Constructor + + public V3ResponseUtil(PSRepositoryInfo repository) : base(repository) + { + this.repository = repository; + } + + #endregion + + #region Overriden Methods + + public override IEnumerable ConvertToPSResourceResult(string[] responses) + { + // in FindHelper: + // serverApi.FindName() -> return responses, and out errRecord + // check outErrorRecord + // + // v3Converter.ConvertToPSResourceInfo(responses) -> return PSResourceResult + // check resourceResult for error, write if needed + + foreach (string response in responses) + { + string parseError = String.Empty; + JsonDocument pkgVersionEntry = null; + try + { + pkgVersionEntry = JsonDocument.Parse(response); + } + catch (Exception e) + { + parseError = e.Message; + } + + if (!String.IsNullOrEmpty(parseError)) + { + yield return new PSResourceResult(returnedObject: null, errorMsg: parseError, isTerminatingError: false); + } + + if (!PSResourceInfo.TryConvertFromJson(pkgVersionEntry, out PSResourceInfo psGetInfo, repository.Name, out string errorMsg)) + { + yield return new PSResourceResult(returnedObject: null, errorMsg: errorMsg, isTerminatingError: false); + } + + yield return new PSResourceResult(returnedObject: psGetInfo, errorMsg: String.Empty, isTerminatingError: false); + } + } + + #endregion + + } +} \ No newline at end of file diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs new file mode 100644 index 000000000..64a83da77 --- /dev/null +++ b/src/code/V3ServerAPICalls.cs @@ -0,0 +1,1072 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.PowerShell.PowerShellGet.UtilClasses; +using NuGet.Versioning; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Linq; +using System.Net; +using System.Text.Json; +using System.Threading.Tasks; +using System.Collections; +using System.Runtime.ExceptionServices; + +namespace Microsoft.PowerShell.PowerShellGet.Cmdlets +{ + internal class V3ServerAPICalls : ServerApiCall + { + #region Members + public override PSRepositoryInfo repository { get; set; } + public override HttpClient s_client { get; set; } + + private static readonly string resourcesName = "resources"; + private static readonly string packageBaseAddressName = "PackageBaseAddress/3.0.0"; + private static readonly string searchQueryServiceName = "SearchQueryService/3.0.0-beta"; + private static readonly string registrationsBaseUrlName = "RegistrationsBaseUrl/Versioned"; + private static readonly string dataName = "data"; + private static readonly string idName = "id"; + private static readonly string versionName = "version"; + private static readonly string tagsName = "tags"; + private static readonly string versionsName = "versions"; + + #endregion + + #region Constructor + + public V3ServerAPICalls(PSRepositoryInfo repository, NetworkCredential networkCredential) : base(repository, networkCredential) + { + this.repository = repository; + + HttpClientHandler handler = new HttpClientHandler() + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + Credentials = networkCredential + }; + + s_client = new HttpClient(handler); + + } + + #endregion + + #region Overriden Methods + // High level design: Find-PSResource >>> IFindPSResource (loops, version checks, etc.) >>> IServerAPICalls (call to repository endpoint/url) + + /// + /// Find method which allows for searching for all packages from a repository and returns latest version for each. + /// Not supported + /// + public override string[] FindAll(bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + string errMsg = $"Find all is not supported for the repository {repository.Uri}"; + edi = ExceptionDispatchInfo.Capture(new OperationNotSupportedException(errMsg)); + + return Utils.EmptyStrArray; + } + + /// + /// Find method which allows for searching for packages with tag from a repository and returns latest version for each. + /// Examples: Search -Tag "Redis" -Repository PSGallery + /// API call: + /// https://azuresearch-ussc.nuget.org/query?q=tags:redis&prerelease=False&semVerLevel=2.0.0 + /// + /// Azure Artifacts does not support querying on tags, so if support this scenario we need to search on the term and then filter + /// + public override string[] FindTag(string tag, bool includePrerelease, ResourceType _type, out ExceptionDispatchInfo edi) + { + List responses = new List(); + + Hashtable resourceUrls = FindResourceType(new string[] { searchQueryServiceName, registrationsBaseUrlName }, out edi); + if (edi != null) + { + return responses.ToArray(); + } + + string searchQueryServiceUrl = resourceUrls[searchQueryServiceName] as string; + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + bool isNuGetRepo = searchQueryServiceUrl.Contains("nuget.org"); + + string query = isNuGetRepo ? $"{searchQueryServiceUrl}?q=tags:{tag.ToLower()}&prerelease={includePrerelease}&semVerLevel=2.0.0" : + $"{searchQueryServiceUrl}?q={tag.ToLower()}&prerelease={includePrerelease}&semVerLevel=2.0.0"; + + // 2) call query with tags. (for Azure artifacts) get unique names, see which ones truly match + JsonElement[] tagPkgs = GetJsonElementArr(query, dataName, out edi); + if (edi != null) + { + return responses.ToArray(); + } + + List matchingResponses = new List(); + string id; + string latestVersion; + foreach (var pkgId in tagPkgs) + { + try + { + if (!pkgId.TryGetProperty(idName, out JsonElement idItem) || !pkgId.TryGetProperty(versionName, out JsonElement versionItem)) + { + string errMsg = $"FindTag(): Id or Version element could not be found in response."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + + id = idItem.ToString(); + latestVersion = versionItem.ToString(); + } + catch (Exception e) + { + string errMsg = $"FindTag(): Id or Version element could not be parsed from response due to exception {e.Message}."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + + // determine if id matches our wildcard criteria + if (isNuGetRepo) + { + string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + matchingResponses.Add(response); + } + else + { + try { + if (!pkgId.TryGetProperty("tags", out JsonElement tagsItem)) + { + string errMsg = $"FindTag(): Tag element could not be found in response."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + + foreach (var tagItem in tagsItem.EnumerateArray()) + { + if (tag.Equals(tagItem.ToString(), StringComparison.InvariantCultureIgnoreCase)) + { + string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + matchingResponses.Add(response); + break; + } + } + } + catch (Exception e) + { + string errMsg = $"FindTag(): Tags element could not be parsed from response due to exception {e.Message}."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + } + } + + return matchingResponses.ToArray(); + } + + public override string[] FindCommandOrDscResource(string tag, bool includePrerelease, bool isSearchingForCommands, out ExceptionDispatchInfo edi) + { + string errMsg = $"Find by CommandName or DSCResource is not supported for {repository.Name} as it uses the V3 server protocol"; + edi = ExceptionDispatchInfo.Capture(new OperationNotSupportedException(errMsg)); + + return Utils.EmptyStrArray; + } + + /// + /// Find method which allows for searching for single name and returns latest version. + /// Name: no wildcard support + /// Examples: Search "Newtonsoft.Json" + /// API call: + /// https://api.nuget.org/v3/registration5-gz-semver2/nuget.server/index.json + /// https://msazure.pkgs.visualstudio.com/One/_packaging/testfeed/nuget/v3/registrations2-semver2/newtonsoft.json/index.json + /// https://msazure.pkgs.visualstudio.com/999aa88e-7ed7-41b2-9d77-5bc261222004/_packaging/0d5429e2-c871-4347-bdc9-d1cbbac5eb3b/nuget/v3/registrations2-semver2/newtonsoft.json/index.json + /// The RegistrationBaseUrl that we're using is "RegistrationBaseUrl/Versioned" + /// This type points to the url to use (ex above) + /// Implementation Note: Need to filter further for latest version (prerelease or non-prerelease dependening on user preference) + /// + public override string FindName(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName, registrationsBaseUrlName }, out edi); + if (edi != null) + { + return String.Empty; + } + + string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + bool isNuGetRepo = packageBaseAddressUrl.Contains("v3-flatcontainer"); + JsonElement[] pkgVersionsArr = GetPackageVersions(packageBaseAddressUrl, packageName, isNuGetRepo, out edi); + if (edi != null) + { + return String.Empty; + } + + string response = string.Empty; + foreach (JsonElement version in pkgVersionsArr) + { + // parse as NuGetVersion + if (NuGetVersion.TryParse(version.ToString(), out NuGetVersion nugetVersion)) + { + /* + * pkgVersion == !prerelease && includePrerelease == true --> keep pkg + * pkgVersion == !prerelease && includePrerelease == false --> keep pkg + * pkgVersion == prerelease && includePrerelease == true --> keep pkg + * pkgVersion == prerelease && includePrerelease == false --> throw away pkg + */ + if (!nugetVersion.IsPrerelease || includePrerelease) + { + response = FindVersionHelper(registrationsBaseUrl, packageName, version.ToString(), out edi); + if (edi != null) + { + return String.Empty; + } + + break; + } + } + } + + if (String.IsNullOrEmpty(response)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindName() with {packageName} returned empty response.")); + } + + return response; + } + + public override string FindNameWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName, registrationsBaseUrlName }, out edi); + if (edi != null) + { + return String.Empty; + } + + string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + bool isNuGetRepo = packageBaseAddressUrl.Contains("v3-flatcontainer"); + JsonElement[] pkgVersionsArr = GetPackageVersions(packageBaseAddressUrl, packageName, isNuGetRepo, out edi); + if (edi != null) + { + return String.Empty; + } + + string response = string.Empty; + foreach (JsonElement version in pkgVersionsArr) + { + // parse as NuGetVersion + if (NuGetVersion.TryParse(version.ToString(), out NuGetVersion nugetVersion)) + { + /* + * pkgVersion == !prerelease && includePrerelease == true --> keep pkg + * pkgVersion == !prerelease && includePrerelease == false --> keep pkg + * pkgVersion == prerelease && includePrerelease == true --> keep pkg + * pkgVersion == prerelease && includePrerelease == false --> throw away pkg + */ + if (!nugetVersion.IsPrerelease || includePrerelease) + { + response = FindVersionHelper(registrationsBaseUrl, packageName, version.ToString(), out edi); + if (edi != null) + { + return String.Empty; + } + + bool isTagMatch = DetermineTagsPresent(response: response, tags: tags, out edi); + + if (!isTagMatch) + { + if (edi == null) + { + string errMsg = $"FindNameWithTag(): Tags required were not found in package {packageName} {version.ToString()}."; + edi = ExceptionDispatchInfo.Capture(new SpecifiedTagsNotFoundException(errMsg)); + } + + return String.Empty; + } + + break; + } + } + } + + if (String.IsNullOrEmpty(response)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindNameWithTag() with {packageName} and tags {String.Join(",", tags)} returned empty response.")); + } + + return response; + } + + /// + /// Find method which allows for searching for single name with wildcards and returns latest version. + /// Name: supports wildcards + /// Examples: Search "Nuget.Server*" + /// API call: + /// - No prerelease: https://api-v2v3search-0.nuget.org/autocomplete?q=storage&prerelease=false + /// - Prerelease: https://api-v2v3search-0.nuget.org/autocomplete?q=storage&prerelease=true + /// + /// https://msazure.pkgs.visualstudio.com/b32aa71e-8ed2-41b2-9d77-5bc261222004/_packaging/0d5429e2-c871-4347-bdc9-d1cbbac5eb3b/nuget/v3/query2?q=Newtonsoft&prerelease=false&semVerLevel=2.0.0 + /// + /// Note: response only returns names + /// + /// Make another query to get the latest version of each package (ie call "FindVersionGlobbing") + /// + public override string[] FindNameGlobbing(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + var names = packageName.Split(new char[] { '*' }, StringSplitOptions.RemoveEmptyEntries); + string querySearchTerm; + + if (names.Length == 0) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name '*' for V3 server protocol repositories is not supported")); + return Utils.EmptyStrArray; + } + if (names.Length == 1) + { + // packageName: *get* -> q: get + // packageName: PowerShell* -> q: PowerShell + // packageName: *ShellGet -> q: ShellGet + querySearchTerm = names[0]; + } + else + { + // *pow*get* + // pow*get -> only support this (V2) + // pow*get* + // *pow*get + + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name with wildcards is only supported for scenarios similar to the following examples: PowerShell*, *ShellGet, *Shell*.")); + return Utils.EmptyStrArray; + } + + // https://msazure.pkgs.visualstudio.com/.../_packaging/.../nuget/v3/query2 (no support for * in search term, but matches like NuGet) + // https://azuresearch-usnc.nuget.org/query?q=Newtonsoft&prerelease=false&semVerLevel=1.0.0 (NuGet) (supports * at end of searchterm q but equivalent to q = text w/o *) + Hashtable resourceUrls = FindResourceType(new string[] { searchQueryServiceName, registrationsBaseUrlName }, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + string searchQueryServiceUrl = resourceUrls[searchQueryServiceName] as string; + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + string query = $"{searchQueryServiceUrl}?q={querySearchTerm}&prerelease={includePrerelease}&semVerLevel=2.0.0"; + + // 2) call query with search term, get unique names, see which ones truly match + JsonElement[] matchingPkgIds = GetJsonElementArr(query, dataName, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + List matchingResponses = new List(); + foreach (var pkgId in matchingPkgIds) + { + string id = string.Empty; + string latestVersion = string.Empty; + + try + { + if (!pkgId.TryGetProperty(idName, out JsonElement idItem) || ! pkgId.TryGetProperty(versionName, out JsonElement versionItem)) + { + string errMsg = $"FindNameGlobbing(): Name or Version element could not be found in response."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + + id = idItem.ToString(); + latestVersion = versionItem.ToString(); + } + catch (Exception e) + { + string errMsg = $"FindNameGlobbing(): Name or Version element could not be parsed from response due to exception {e.Message}."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + break; + } + + // determine if id matches our wildcard criteria + if ((packageName.StartsWith("*") && packageName.EndsWith("*") && id.ToLower().Contains(querySearchTerm.ToLower())) || + (packageName.EndsWith("*") && id.StartsWith(querySearchTerm, StringComparison.OrdinalIgnoreCase)) || + (packageName.StartsWith("*") && id.EndsWith(querySearchTerm, StringComparison.OrdinalIgnoreCase))) + { + string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); + + if (edi != null) + { + return Utils.EmptyStrArray; + } + + matchingResponses.Add(response); + } + } + + return matchingResponses.ToArray(); + } + + public override string[] FindNameGlobbingWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + var names = packageName.Split(new char[] { '*' }, StringSplitOptions.RemoveEmptyEntries); + string querySearchTerm; + + if (names.Length == 0) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name '*' for V3 server protocol repositories is not supported")); + return Utils.EmptyStrArray; + } + if (names.Length == 1) + { + // packageName: *get* -> q: get + // packageName: PowerShell* -> q: PowerShell + // packageName: *ShellGet -> q: ShellGet + querySearchTerm = names[0]; + } + else + { + // *pow*get* + // pow*get -> only support this (V2) + // pow*get* + // *pow*get + + edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name with wildcards is only supported for scenarios similar to the following examples: PowerShell*, *ShellGet, *Shell*.")); + return Utils.EmptyStrArray; + } + + // https://msazure.pkgs.visualstudio.com/.../_packaging/.../nuget/v3/query2 (no support for * in search term, but matches like NuGet) + // https://azuresearch-usnc.nuget.org/query?q=Newtonsoft&prerelease=false&semVerLevel=1.0.0 (NuGet) (supports * at end of searchterm q but equivalent to q = text w/o *) + Hashtable resourceUrls = FindResourceType(new string[] { searchQueryServiceName, registrationsBaseUrlName }, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + string searchQueryServiceUrl = resourceUrls[searchQueryServiceName] as string; + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + string query = $"{searchQueryServiceUrl}?q={querySearchTerm}&prerelease={includePrerelease}&semVerLevel=2.0.0"; + + // 2) call query with search term, get unique names, see which ones truly match + JsonElement[] matchingPkgIds = GetJsonElementArr(query, dataName, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + List matchingResponses = new List(); + foreach (var pkgId in matchingPkgIds) + { + string id = string.Empty; + string latestVersion = string.Empty; + string[] pkgTags = Utils.EmptyStrArray; + + try + { + if (!pkgId.TryGetProperty(idName, out JsonElement idItem) || !pkgId.TryGetProperty(versionName, out JsonElement versionItem)) + { + string errMsg = $"FindNameGlobbing(): Name or Version element could not be found in response."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + + if (!pkgId.TryGetProperty(tagsName, out JsonElement tagsItem)) + { + string errMsg = $"FindNameGlobbing(): Tags element could not be found in response."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return Utils.EmptyStrArray; + } + + id = idItem.ToString(); + latestVersion = versionItem.ToString(); + + pkgTags = GetTagsFromJsonElement(tagsElement: tagsItem); + } + catch (Exception e) + { + string errMsg = $"FindNameGlobbingWithTag(): Name or Version or Tags element could not be parsed from response due to exception {e.Message}."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + break; + } + + // determine if id matches our wildcard criteria + if ((packageName.StartsWith("*") && packageName.EndsWith("*") && id.ToLower().Contains(querySearchTerm.ToLower())) || + (packageName.EndsWith("*") && id.StartsWith(querySearchTerm, StringComparison.OrdinalIgnoreCase)) || + (packageName.StartsWith("*") && id.EndsWith(querySearchTerm, StringComparison.OrdinalIgnoreCase))) + { + bool isTagMatch = DeterminePkgTagsSatisfyRequiredTags(pkgTags: pkgTags, requiredTags: tags); + if (!isTagMatch) + { + continue; + } + + string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); + + if (edi != null) + { + continue; + } + + matchingResponses.Add(response); + } + } + + return matchingResponses.ToArray(); + } + + /// + /// Find method which allows for searching for single name with version range. + /// Name: no wildcard support + /// Version: supports wildcards + /// Examples: Search "NuGet.Server.Core" "[1.0.0.0, 5.0.0.0]" + /// Search "NuGet.Server.Core" "3.*" + /// API Call: + /// then, find all versions for a pkg + /// for nuget: + /// this contains all pkg version info: https://api.nuget.org/v3/registration5-gz-semver2/nuget.server/index.json + /// However, we will use the flattened version list: https://api.nuget.org/v3-flatcontainer/newtonsoft.json/index.json + /// for Azure Artifacts: + /// https://msazure.pkgs.visualstudio.com/b32aa71e-8ed2-41b2-9d77-5bc261222004/_packaging/0d5429e2-c871-4347-bdc9-d1cbbac5eb3b/nuget/v3/flat2/newtonsoft.json/index.json + /// (azure artifacts) + /// + /// Note: very different responses for nuget vs azure artifacts + /// + /// After we figure out what version we want, call "FindVersion" (or some helper method) + /// need to filter client side + /// Implementation note: Returns all versions, including prerelease ones. Later (in the API client side) we'll do filtering on the versions to satisfy what user provided. + /// + public override string[] FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName, registrationsBaseUrlName }, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + bool isNuGetRepo = packageBaseAddressUrl.Contains("v3-flatcontainer"); + JsonElement[] pkgVersionsArr = GetPackageVersions(packageBaseAddressUrl, packageName, isNuGetRepo, out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + List responses = new List(); + foreach (var version in pkgVersionsArr) { + if (NuGetVersion.TryParse(version.ToString(), out NuGetVersion nugetVersion) && versionRange.Satisfies(nugetVersion)) + { + /* + * pkgVersion == !prerelease && includePrerelease == true --> keep pkg + * pkgVersion == !prerelease && includePrerelease == false --> keep pkg + * pkgVersion == prerelease && includePrerelease == true --> keep pkg + * pkgVersion == prerelease && includePrerelease == false --> throw away pkg + */ + if (!nugetVersion.IsPrerelease || includePrerelease) { + string response = FindVersionHelper(registrationsBaseUrl, packageName, version.ToString(), out edi); + if (edi != null) + { + return Utils.EmptyStrArray; + } + + responses.Add(response); + } + } + } + + return responses.ToArray(); + } + + /// + /// Find method which allows for searching for single name with specific version. + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "NuGet.Server.Core" "3.0.0-beta" + /// API call: + /// first find the RegistrationBaseUrl + /// https://api.nuget.org/v3/registration5-gz-semver2/nuget.server/index.json + /// + /// https://msazure.pkgs.visualstudio.com/One/_packaging/testfeed/nuget/v3/registrations2-semver2/newtonsoft.json/index.json + /// https://msazure.pkgs.visualstudio.com/999aa88e-7ed7-41b2-9d77-5bc261222004/_packaging/0d5429e2-c871-4347-bdc9-d1cbbac5eb3b/nuget/v3/registrations2-semver2/newtonsoft.json/index.json + /// The RegistrationBaseUrl that we're using is "RegistrationBaseUrl/Versioned" + /// This type points to the url to use (ex above) + /// + /// then we can make a call for the specific version + /// https://api.nuget.org/v3/registration5-gz-semver2/nuget.server.core/3.0.0-beta + /// (alternative url for nuget gallery): https://api.nuget.org/v3/registration5-gz-semver2/nuget.server.core/index.json#page/3.0.0-beta/3.0.0-beta + /// https://msazure.pkgs.visualstudio.com/b32aa71e-8ed2-41b2-9d77-5bc261222004/_packaging/0d5429e2-c871-4347-bdc9-d1cbbac5eb3b/nuget/v3/registrations2/newtonsoft.json/13.0.2.json + /// + /// + public override string FindVersion(string packageName, string version, ResourceType type, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { registrationsBaseUrlName }, out edi); + if (edi != null) + { + return String.Empty; + } + + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + string response = FindVersionHelper(registrationsBaseUrl, packageName, version, out edi); + if (edi != null) + { + return String.Empty; + } + + if (String.IsNullOrEmpty(response)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindVersion() with {packageName} and version {version} returned empty response.")); + } + + return response; + } + + public override string FindVersionWithTag(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { registrationsBaseUrlName }, out edi); + if (edi != null) + { + return String.Empty; + } + + string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + + string response = FindVersionHelper(registrationsBaseUrl, packageName, version, out edi); + if (edi != null) + { + return String.Empty; + } + + bool isTagMatch = DetermineTagsPresent(response: response, tags: tags, out edi); + + if (!isTagMatch) + { + if (edi == null) + { + string errMsg = $"FindVersionWithTag(): Tags required were not found in package {packageName} {version.ToString()}."; + edi = ExceptionDispatchInfo.Capture(new SpecifiedTagsNotFoundException(errMsg)); + } + + return String.Empty; + } + + if (String.IsNullOrEmpty(response)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindVersion() with {packageName}, tags {String.Join(", ", tags)} and version {version} returned empty response.")); + } + + return response; + } + + + /** INSTALL APIS **/ + + /// + /// Installs specific package. + /// Name: no wildcard support. + /// Examples: Install "PowerShellGet" + /// Implementation Note: if not prerelease: https://www.powershellgallery.com/api/v2/package/powershellget (Returns latest stable) + /// if prerelease, the calling method should first call IFindPSResource.FindName(), + /// then find the exact version to install, then call into install version + /// + public override HttpContent InstallName(string packageName, bool includePrerelease, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName }, out edi); + if (edi != null) + { + return null; + } + + string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; + + bool isNuGetRepo = packageBaseAddressUrl.Contains("v3-flatcontainer"); + + JsonElement[] pkgVersionsArr = GetPackageVersions(packageBaseAddressUrl, packageName, isNuGetRepo, out edi); + if (edi != null) + { + return null; + } + + foreach (JsonElement version in pkgVersionsArr) + { + if (NuGetVersion.TryParse(version.ToString(), out NuGetVersion nugetVersion)) + { + /* + * pkgVersion == !prerelease && includePrerelease == true --> keep pkg + * pkgVersion == !prerelease && includePrerelease == false --> keep pkg + * pkgVersion == prerelease && includePrerelease == true --> keep pkg + * pkgVersion == prerelease && includePrerelease == false --> throw away pkg + */ + if (!nugetVersion.IsPrerelease || includePrerelease) + { + var response = InstallVersion(packageName, version.ToString(), out edi); + if (edi != null) + { + return null; + } + + return response; + } + } + } + + return null; + } + + /// + /// Installs package with specific name and version. + /// Name: no wildcard support. + /// Version: no wildcard support. + /// Examples: Install "PowerShellGet" -Version "3.0.0.0" + /// Install "PowerShellGet" -Version "3.0.0-beta16" + /// + /// https://api.nuget.org/v3-flatcontainer/newtonsoft.json/9.0.1/newtonsoft.json.9.0.1.nupkg + /// API Call: + /// + public override HttpContent InstallVersion(string packageName, string version, out ExceptionDispatchInfo edi) + { + Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName }, out edi); + if (edi != null) + { + return null; + } + + string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; + + string pkgName = packageName.ToLower(); + string installPkgUrl = $"{packageBaseAddressUrl}{pkgName}/{version}/{pkgName}.{version}.nupkg"; + + var content = HttpRequestCallForContent(installPkgUrl, out edi); + if (edi != null) + { + return null; + } + + return content; + } + + #endregion + + #region Private Methods + + private String HttpRequestCall(string requestUrlV3, out ExceptionDispatchInfo edi) + { + edi = null; + string response = string.Empty; + + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV3); + + response = SendV3RequestAsync(request, s_client).GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (ArgumentNullException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (InvalidOperationException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (Exception e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + + return response; + } + + private HttpContent HttpRequestCallForContent(string requestUrlV3, out ExceptionDispatchInfo edi) + { + edi = null; + HttpContent content = null; + + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV3); + + content = SendV3RequestForContentAsync(request, s_client).GetAwaiter().GetResult(); + } + catch (HttpRequestException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (ArgumentNullException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + catch (InvalidOperationException e) + { + edi = ExceptionDispatchInfo.Capture(e); + } + + return content; + } + + private Hashtable FindResourceType(string[] resourceTypeName, out ExceptionDispatchInfo edi) + { + Hashtable resourceHash = new Hashtable(); + JsonElement[] resources = GetJsonElementArr($"{repository.Uri}", resourcesName, out edi); + if (edi != null) + { + return resourceHash; + } + + foreach (JsonElement resource in resources) + { + try + { + if (resource.TryGetProperty("@type", out JsonElement typeElement) && resourceTypeName.Contains(typeElement.ToString())) + { + // check if key already present in hastable, as there can be resources with same type but primary/secondary instances + if (!resourceHash.ContainsKey(typeElement.ToString())) + { + if (resource.TryGetProperty("@id", out JsonElement idElement)) + { + // add name of the resource and its url + resourceHash.Add(typeElement.ToString(), idElement.ToString()); + } + else + { + string errMsg = $"@type element was found but @id element not found in service index '{repository.Uri}' for {resourceTypeName}."; + edi = ExceptionDispatchInfo.Capture(new V3ResourceNotFoundException(errMsg)); + return resourceHash; + } + } + } + } + catch (Exception e) + { + string errMsg = $"Exception parsing JSON for respository {repository.Uri} with error: {e.Message}"; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return resourceHash; + } + + if (resourceHash.Count == resourceTypeName.Length) + { + break; + } + } + + foreach (string resourceType in resourceTypeName) + { + if (!resourceHash.ContainsKey(resourceType)) + { + string errMsg = $"FindResourceType(): Could not find resource type {resourceType} from the service index."; + edi = ExceptionDispatchInfo.Capture(new V3ResourceNotFoundException(errMsg)); + break; + } + } + + return resourceHash; + } + + private string FindVersionHelper(string registrationsBaseUrl, string packageName, string version, out ExceptionDispatchInfo edi) + { + // https://api.nuget.org/v3/registration5-gz-semver2/newtonsoft.json/13.0.2.json + var requestPkgMapping = $"{registrationsBaseUrl}{packageName.ToLower()}/{version}.json"; + string pkgMappingResponse = HttpRequestCall(requestPkgMapping, out edi); + if (edi != null) + { + return String.Empty; + } + + string catalogEntryUrl = string.Empty; + try + { + JsonDocument pkgMappingDom = JsonDocument.Parse(pkgMappingResponse); + JsonElement rootPkgMappingDom = pkgMappingDom.RootElement; + + if (!rootPkgMappingDom.TryGetProperty("catalogEntry", out JsonElement catalogEntryUrlElement)) + { + string errMsg = $"FindVersionHelper(): CatalogEntry element could not be found in response or was empty."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return String.Empty; + } + + catalogEntryUrl = catalogEntryUrlElement.ToString(); + } + catch (Exception e) + { + string errMsg = $"FindVersionHelper(): Exception parsing JSON for respository {repository.Uri} with error: {e.Message}"; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return String.Empty; + } + + string response = HttpRequestCall(catalogEntryUrl, out edi); + if (edi != null) + { + return String.Empty; + } + + return response; + } + + private bool DetermineTagsPresent(string response, string[] tags, out ExceptionDispatchInfo edi) + { + edi = null; + string[] pkgTags = Utils.EmptyStrArray; + + try + { + JsonDocument pkgMappingDom = JsonDocument.Parse(response); + JsonElement rootPkgMappingDom = pkgMappingDom.RootElement; + + if (!rootPkgMappingDom.TryGetProperty(tagsName, out JsonElement tagsElement)) + { + string errMsg = $"FindNameWithTag(): Tags element could not be found in response or was empty."; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return false; + } + + pkgTags = GetTagsFromJsonElement(tagsElement: tagsElement); + } + catch (Exception e) + { + string errMsg = $"DetermineTagsPresent(): Exception parsing JSON for respository {repository.Uri} with error: {e.Message}"; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return false; + } + + bool isTagMatch = DeterminePkgTagsSatisfyRequiredTags(pkgTags: pkgTags, requiredTags: tags); + + return isTagMatch; + } + + private string[] GetTagsFromJsonElement(JsonElement tagsElement) + { + List tagsFound = new List(); + JsonElement[] pkgTagElements = tagsElement.EnumerateArray().ToArray(); + foreach (JsonElement tagItem in pkgTagElements) + { + tagsFound.Add(tagItem.ToString().ToLower()); + } + + return tagsFound.ToArray(); + } + + private bool DeterminePkgTagsSatisfyRequiredTags(string[] pkgTags, string[] requiredTags) + { + bool isTagMatch = true; + + foreach (string tag in requiredTags) + { + if (!pkgTags.Contains(tag.ToLower())) + { + isTagMatch = false; + break; + } + } + + return isTagMatch; + } + + private JsonElement[] GetPackageVersions(string packageBaseAddressUrl, string packageName, bool isNuGetRepo, out ExceptionDispatchInfo edi) + { + if (String.IsNullOrEmpty(packageBaseAddressUrl)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"GetPackageVersions(): Package Base URL cannot be null or empty")); + return new JsonElement[]{}; + } + + JsonElement[] pkgVersionsElement = GetJsonElementArr($"{packageBaseAddressUrl}{packageName.ToLower()}/index.json", versionsName, out edi); + if (edi != null) + { + return new JsonElement[]{}; + } + + return isNuGetRepo ? pkgVersionsElement.Reverse().ToArray() : pkgVersionsElement.ToArray(); + } + + private JsonElement[] GetJsonElementArr(string request, string propertyName, out ExceptionDispatchInfo edi) + { + JsonElement[] pkgsArr = new JsonElement[0]; + try + { + string response = HttpRequestCall(request, out edi); + if (edi != null) + { + return new JsonElement[]{}; + } + + JsonDocument pkgsDom = JsonDocument.Parse(response); + + pkgsDom.RootElement.TryGetProperty(propertyName, out JsonElement pkgs); + + pkgsArr = pkgs.EnumerateArray().ToArray(); + } + catch (Exception e) + { + string errMsg = $"Exception parsing JSON for respository {repository.Uri} with error: {e.Message}"; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + } + + return pkgsArr; + } + + public static async Task SendV3RequestAsync(HttpRequestMessage message, HttpClient s_client) + { + string errMsg = "SendV3RequestAsync(): Error occured while trying to retrieve response: "; + + try + { + HttpResponseMessage response = await s_client.SendAsync(message); + response.EnsureSuccessStatusCode(); + + var responseStr = await response.Content.ReadAsStringAsync(); + + return responseStr; + } + catch (HttpRequestException e) + { + throw new HttpRequestException(errMsg + e.Message); + } + catch (ArgumentNullException e) + { + throw new ArgumentNullException(errMsg + e.Message); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException(errMsg + e.Message); + } + } + + + public static async Task SendV3RequestForContentAsync(HttpRequestMessage message, HttpClient s_client) + { + string errMsg = "SendV3RequestForContentAsync(): Error occured while trying to retrieve response for content: "; + + try + { + HttpResponseMessage response = await s_client.SendAsync(message); + response.EnsureSuccessStatusCode(); + return response.Content; + } + catch (HttpRequestException e) + { + throw new HttpRequestException(errMsg + e.Message); + } + catch (ArgumentNullException e) + { + throw new ArgumentNullException(errMsg + e.Message); + } + catch (InvalidOperationException e) + { + throw new InvalidOperationException(errMsg + e.Message); + } + } + + #endregion + } +} diff --git a/test/FindPSResource.Tests.ps1 b/test/FindPSResource.Tests.ps1 deleted file mode 100644 index 53449b2d9..000000000 --- a/test/FindPSResource.Tests.ps1 +++ /dev/null @@ -1,381 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force - -Describe 'Test Find-PSResource for Module' { - - BeforeAll{ - $PSGalleryName = Get-PSGalleryName - $NuGetGalleryName = Get-NuGetGalleryName - $testModuleName = "test_module" - $testScriptName = "test_script" - $commandName = "Get-TargetResource" - $dscResourceName = "SystemLocale" - $parentModuleName = "SystemLocaleDsc" - Get-NewPSResourceRepositoryFile - Register-LocalRepos - } - - AfterAll { - Get-RevertPSResourceRepositoryFile - } - - It "find Specific Module Resource by Name" { - $specItem = Find-PSResource -Name $testModuleName - $specItem.Name | Should -Be $testModuleName - } - - It "should not find resource given nonexistant name" { - $res = Find-PSResource -Name NonExistantModule - $res | Should -BeNullOrEmpty - } - - It "should not find any resources given names with invalid wildcard characters" { - Find-PSResource -Name "Invalid?PkgName", "Invalid[PkgName" -ErrorVariable err -ErrorAction SilentlyContinue - $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "ErrorFilteringNamesForUnsupportedWildcards,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" - } - - It "find resources when Name contains * from V2 endpoint repository (PowerShellGallery))" { - $foundScript = $False - $res = Find-PSResource -Name "AzureS*" -Repository $PSGalleryName - $res.Count | Should -BeGreaterThan 1 - # should find Module and Script resources - foreach ($item in $res) - { - if ($item.Type -eq "Script") - { - $foundScript = $true - } - } - - $foundScript | Should -BeTrue - } - - # TODO: get working with local repo - # It "should find all resources given Name that equals wildcard, '*'" { - # $repoName = "psgettestlocal" - # Get-ModuleResourcePublishedToLocalRepoTestDrive "TestLocalModule1" $repoName - # Get-ModuleResourcePublishedToLocalRepoTestDrive "TestLocalModule2" $repoName - # Get-ModuleResourcePublishedToLocalRepoTestDrive "TestLocalModule3" $repoName - - # $foundResources = Find-PSResource -Name "TestLocalModule1","TestLocalModule2","TestLocalModule3" -Repository $repoName - # # TODO: wildcard search is not supported with local repositories, from NuGet protocol API side- ask about this. - # # $foundResources = Find-PSResource -Name "*" -Repository $repoName - # $foundResources.Count | Should -Not -Be 0 - - # # Should find Module and Script resources but no prerelease resources - # $foundResources | where-object Name -eq "TestLocalModule1" | Should -Not -BeNullOrEmpty -Because "TestLocalModule1 should exist in local repo" - # $foundResources | where-object Name -eq "test_script" | Should -Not -BeNullOrEmpty -Because "TestLocalScript1 should exist in local repo" - # $foundResources | where-object IsPrerelease -eq $true | Should -BeNullOrEmpty -Because "No prerelease resources should be returned" - # } - - # # TODO: get working with local repo - # It "should find all resources (including prerelease) given Name that equals wildcard, '*' and Prerelease parameter" { - # Get-ModuleResourcePublishedToLocalRepoTestDrive "TestLocalModule1" $repoName - # Get-ModuleResourcePublishedToLocalRepoTestDrive "TestLocalModule2" $repoName - # Get-ModuleResourcePublishedToLocalRepoTestDrive "TestLocalModule3" $repoName - # $foundResources = Find-PSResource -Name "*" -Prerelease -Repository $repoName - - # # Should find Module and Script resources inlcuding prerelease resources - # $foundResources | where-object Name -eq "test_module" | Should -Not -BeNullOrEmpty -Because "test_module should exist in local repo" - # $foundResources | where-object Name -eq "test_script" | Should -Not -BeNullOrEmpty -Because "test_script should exist in local repo" - # $foundResources | where-object IsPrerelease -eq $true | Should -Not -BeNullOrEmpty -Because "Prerelease resources should be returned" - # } - - It "find resource given Name from V3 endpoint repository (NuGetGallery)" { - $res = Find-PSResource -Name "Serilog" -Repository $NuGetGalleryName - $res.Count | Should -Be 1 - $res.Name | Should -Be "Serilog" - $res.Repository | Should -Be $NuGetGalleryName - } - - It "find resources when Name contains wildcard * from V3 endpoint repository" { - $res = Find-PSResource -Name "Serilog*" -Repository $NuGetGalleryName - $res.Count | Should -BeGreaterThan 1 - } - - $testCases2 = @{Version="[5.0.0.0]"; ExpectedVersions=@("5.0.0.0"); Reason="validate version, exact match"}, - @{Version="5.0.0.0"; ExpectedVersions=@("5.0.0.0"); Reason="validate version, exact match without bracket syntax"}, - @{Version="[1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0", "5.0.0.0"); Reason="validate version, exact range inclusive"}, - @{Version="(1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("3.0.0.0"); Reason="validate version, exact range exclusive"}, - @{Version="(1.0.0.0,)"; ExpectedVersions=@("3.0.0.0", "5.0.0.0"); Reason="validate version, minimum version exclusive"}, - @{Version="[1.0.0.0,)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0", "5.0.0.0"); Reason="validate version, minimum version inclusive"}, - @{Version="(,3.0.0.0)"; ExpectedVersions=@("1.0.0.0"); Reason="validate version, maximum version exclusive"}, - @{Version="(,3.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, maximum version inclusive"}, - @{Version="[1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} - @{Version="(1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("3.0.0.0", "5.0.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} - - It "find resource when given Name to " -TestCases $testCases2{ - param($Version, $ExpectedVersions) - $res = Find-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName - foreach ($item in $res) { - $item.Name | Should -Be $testModuleName - $ExpectedVersions | Should -Contain $item.Version - } - } - - It "not find resource with incorrectly formatted version such as " -TestCases @( - @{Version='(1.0.0.0)'; Description="exclusive version (2.5.0.0)"}, - @{Version='[1-0-0-0]'; Description="version formatted with invalid delimiter"} - ) { - param($Version, $Description) - - $res = Find-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName 2>$null - $res | Should -BeNullOrEmpty - } - - $testCases = @{Version='[1.*.0.0]'; Description="version with wilcard in middle"}, - @{Version='[*.0.0.0]'; Description="version with wilcard at start"}, - @{Version='[1.0.*.0]'; Description="version with wildcard at third digit"} - @{Version='[1.0.0.*'; Description="version with wildcard at end"}, - @{Version='[1..0.0]'; Description="version with missing digit in middle"}, - @{Version='[1.0.0.]'; Description="version with missing digit at end"}, - @{Version='[1.0.0.0.0]'; Description="version with more than 4 digits"} - - It "not find resource and throw exception with incorrectly formatted version such as " -TestCases $testCases { - param($Version, $Description) - - Find-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue - $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" - } - - It "find all versions of resource when given Name, Version not null --> '*'" { - $res = Find-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName - $res | ForEach-Object { - $_.Name | Should -Be $testModuleName - } - $res.Count | Should -BeGreaterOrEqual 1 - } - - It "find resources when given Name with wildcard, Version not null --> '*'" { - $res = Find-PSResource -Name "TestModuleWithDependency*" -Version "*" -Repository $PSGalleryName - $moduleA = $res | Where-Object {$_.Name -eq "TestModuleWithDependencyA"} - $moduleA.Count | Should -BeGreaterOrEqual 3 - $moduleB = $res | Where-Object {$_.Name -eq "TestModuleWithDependencyB"} - $moduleB.Count | Should -BeGreaterOrEqual 2 - $moduleC = $res | Where-Object {$_.Name -eq "TestModuleWithDependencyC"} - $moduleC.Count | Should -BeGreaterOrEqual 3 - $moduleD = $res | Where-Object {$_.Name -eq "TestModuleWithDependencyD"} - $moduleD.Count | Should -BeGreaterOrEqual 2 - $moduleE = $res | Where-Object {$_.Name -eq "TestModuleWithDependencyE"} - $moduleE.Count | Should -BeGreaterOrEqual 1 - $moduleF = $res | Where-Object {$_.Name -eq "TestModuleWithDependencyF"} - $moduleF.Count | Should -BeGreaterOrEqual 1 - } - - It "find resources when given Name with wildcard, Version range" { - $res = Find-PSResource -Name "TestModuleWithDependency*" -Version "[1.0.0.0, 5.0.0.0]" -Repository $PSGalleryName - foreach ($pkg in $res) { - $pkg.Name | Should -Match "TestModuleWithDependency*" - [System.Version]$pkg.Version -ge [System.Version]"1.0.0.0" -or [System.Version]$pkg.Version -le [System.Version]"5.0.0.0" | Should -Be $true - } - } - - It "find resource when given Name, Version param null" { - $res = Find-PSResource -Name $testModuleName -Repository $PSGalleryName - $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "5.0.0.0" - } - - It "find resource with latest (including prerelease) version given Prerelease parameter" { - # test_module resource's latest version is a prerelease version, before that it has a non-prerelease version - $res = Find-PSResource -Name $testModuleName -Repository $PSGalleryName - $res.Version | Should -Be "5.0.0.0" - - $resPrerelease = Find-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName - $resPrerelease.Version | Should -Be "5.2.5.0" - $resPrerelease.Prerelease | Should -Be "alpha001" - } - - It "find resources, including Prerelease version resources, when given Prerelease parameter" { - $resWithoutPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName - $resWithPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName - $resWithPrerelease.Count | Should -BeGreaterOrEqual $resWithoutPrerelease.Count - } - - It "find resource and its dependency resources with IncludeDependencies parameter" { - $resWithoutDependencies = Find-PSResource -Name "TestModuleWithDependencyE" -Repository $PSGalleryName - $resWithoutDependencies.Count | Should -Be 1 - $resWithoutDependencies.Name | Should -Be "TestModuleWithDependencyE" - - # TestModuleWithDependencyE has the following dependencies: - # TestModuleWithDependencyC <= 1.0.0.0 - # TestModuleWithDependencyB >= 1.0.0.0 - # TestModuleWithDependencyD <= 1.0.0.0 - - $resWithDependencies = Find-PSResource -Name "TestModuleWithDependencyE" -IncludeDependencies -Repository $PSGalleryName - $resWithDependencies.Count | Should -BeGreaterThan $resWithoutDependencies.Count - - $foundParentPkgE = $false - $foundDepB = $false - $foundDepBCorrectVersion = $false - $foundDepC = $false - $foundDepCCorrectVersion = $false - $foundDepD = $false - $foundDepDCorrectVersion = $false - foreach ($pkg in $resWithDependencies) - { - if ($pkg.Name -eq "TestModuleWithDependencyE") - { - $foundParentPkgE = $true - } - elseif ($pkg.Name -eq "TestModuleWithDependencyC") - { - $foundDepC = $true - $foundDepCCorrectVersion = [System.Version]$pkg.Version -le [System.Version]"1.0.0.0" - } - elseif ($pkg.Name -eq "TestModuleWithDependencyB") - { - $foundDepB = $true - $foundDepBCorrectVersion = [System.Version]$pkg.Version -ge [System.Version]"3.0.0.0" - } - elseif ($pkg.Name -eq "TestModuleWithDependencyD") - { - $foundDepD = $true - $foundDepDCorrectVersion = [System.Version]$pkg.Version -le [System.Version]"1.0.0.0" - } - } - - $foundParentPkgE | Should -Be $true - $foundDepC | Should -Be $true - $foundDepCCorrectVersion | Should -Be $true - $foundDepB | Should -Be $true - $foundDepBCorrectVersion | Should -Be $true - $foundDepD | Should -Be $true - $foundDepDCorrectVersion | Should -Be $true - } - - It "find resource of Type script or module from PSGallery, when no Type parameter provided" { - $resScript = Find-PSResource -Name $testScriptName -Repository $PSGalleryName - $resScript.Name | Should -Be $testScriptName - $resScriptType = Out-String -InputObject $resScript.Type - $resScriptType.Replace(",", " ").Split() | Should -Contain "Script" - - $resModule = Find-PSResource -Name $testModuleName -Repository $PSGalleryName - $resModule.Name | Should -Be $testModuleName - $resModuleType = Out-String -InputObject $resModule.Type - $resModuleType.Replace(",", " ").Split() | Should -Contain "Module" - } - - It "find resource of Type Script from PSGallery, when Type Script specified" { - $resScript = Find-PSResource -Name $testScriptName -Repository $PSGalleryName -Type "Script" - $resScript.Name | Should -Be $testScriptName - $resScript.Repository | Should -Be "PSGalleryScripts" - $resScriptType = Out-String -InputObject $resScript.Type - $resScriptType.Replace(",", " ").Split() | Should -Contain "Script" - } - - It "find resource of Type Command from PSGallery, when Type Command specified" { - $resources = Find-PSResource -Name "AzureS*" -Repository $PSGalleryName -Type "Command" - foreach ($item in $resources) { - $resType = Out-String -InputObject $item.Type - $resType.Replace(",", " ").Split() | Should -Contain "Command" - } - } - - It "find all resources of Type Module when Type parameter set is used" -Skip { - $foundScript = $False - $res = Find-PSResource -Name "test*" -Type Module -Repository $PSGalleryName - $res.Count | Should -BeGreaterThan 1 - foreach ($item in $res) { - if ($item.Type -eq "Script") - { - $foundScript = $True - } - } - - $foundScript | Should -Be $False - } - - It "find resources given Tag parameter" { - $resWithEitherExpectedTag = @("NetworkingDsc", "DSCR_FileContent", "SS.PowerShell") - $res = Find-PSResource -Name "NetworkingDsc", "HPCMSL", "DSCR_FileContent", "SS.PowerShell", "PowerShellGet" -Tag "Dsc", "json" -Repository $PSGalleryName - foreach ($item in $res) { - $resWithEitherExpectedTag | Should -Contain $item.Name - } - } - - It "find all resources with specified tag given Tag property" { - $foundTestModule = $False - $foundTestScript = $False - $tagToFind = "Tag2" - $res = Find-PSResource -Tag $tagToFind -Repository $PSGalleryName - foreach ($item in $res) { - $item.Tags -contains $tagToFind | Should -Be $True - - if ($item.Name -eq $testModuleName) - { - $foundTestModule = $True - } - - if ($item.Name -eq $testScriptName) - { - $foundTestScript = $True - } - } - - $foundTestModule | Should -Be $True - $foundTestScript | Should -Be $True - } - - It "find resource in local repository given Repository parameter" { - $publishModuleName = "TestFindModule" - $repoName = "psgettestlocal" - Get-ModuleResourcePublishedToLocalRepoTestDrive $publishModuleName $repoName - - $res = Find-PSResource -Name $publishModuleName -Repository $repoName - $res | Should -Not -BeNullOrEmpty - $res.Name | Should -Be $publishModuleName - $res.Repository | Should -Be $repoName - } - - It "find Resource given repository parameter, where resource exists in multiple local repos" { - $moduleName = "test_local_mod" - $repoHigherPriorityRanking = "psgettestlocal" - $repoLowerPriorityRanking = "psgettestlocal2" - - Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $repoHigherPriorityRanking - Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $repoLowerPriorityRanking - - $res = Find-PSResource -Name $moduleName - $res.Repository | Should -Be $repoHigherPriorityRanking - - $resNonDefault = Find-PSResource -Name $moduleName -Repository $repoLowerPriorityRanking - $resNonDefault.Repository | Should -Be $repoLowerPriorityRanking - } - - # # Skip test for now because it takes too run (132.24 sec) - # It "find resource given CommandName (CommandNameParameterSet)" -Skip { - # $res = Find-PSResource -CommandName $commandName -Repository $PSGalleryName - # foreach ($item in $res) { - # $item.Name | Should -Be $commandName - # $item.ParentResource.Includes.Command | Should -Contain $commandName - # } - # } - - It "find resource given CommandName and ModuleName (CommandNameParameterSet)" { - $res = Find-PSResource -CommandName $commandName -ModuleName $parentModuleName -Repository $PSGalleryName - $res.Name | Should -Be $commandName - $res.ParentResource.Name | Should -Be $parentModuleName - $res.ParentResource.Includes.Command | Should -Contain $commandName - } - - # Skip test for now because it takes too long to run (> 60 sec) - # It "find resource given DSCResourceName (DSCResourceNameParameterSet)" -Skip { - # $res = Find-PSResource -DscResourceName $dscResourceName -Repository $PSGalleryName - # foreach ($item in $res) { - # $item.Name | Should -Be $dscResourceName - # $item.ParentResource.Includes.DscResource | Should -Contain $dscResourceName - # } - # } - - It "find resource given DscResourceName and ModuleName (DSCResourceNameParameterSet)" { - $res = Find-PSResource -DscResourceName $dscResourceName -ModuleName $parentModuleName -Repository $PSGalleryName - $res.Name | Should -Be $dscResourceName - $res.ParentResource.Name | Should -Be $parentModuleName - $res.ParentResource.Includes.DscResource | Should -Contain $dscResourceName - } -} diff --git a/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 new file mode 100644 index 000000000..65c7e0fff --- /dev/null +++ b/test/FindPSResourceTests/FindPSResourceLocal.Tests.ps1 @@ -0,0 +1,208 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Find-PSResource for Module' { + + BeforeAll{ + $localRepo = "psgettestlocal" + $testModuleName = "test_local_mod" + $testModuleName2 = "test_local_mod2" + $commandName = "cmd1" + $dscResourceName = "dsc1" + $cmdName = "PSCommand_$commandName" + $dscName = "PSDscResource_$dscResourceName" + $prereleaseLabel = "" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + + $tags = @("Test", "Tag2", $cmdName, $dscName) + Get-ModuleResourcePublishedToLocalRepoTestDrive $testModuleName $localRepo "1.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $testModuleName $localRepo "3.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $testModuleName $localRepo "5.0.0" $prereleaseLabel $tags + Get-ModuleResourcePublishedToLocalRepoTestDrive $testModuleName2 $localRepo "5.0.0" $prereleaseLabel $tags + + $prereleaseLabel = "alpha001" + $params = @{ + moduleName = $testModuleName + repoName = $localRepo + packageVersion = "5.2.5" + prereleaseLabel = $prereleaseLabel + tags = $tags + } + Get-ModuleResourcePublishedToLocalRepoTestDrive @params + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "find resource given specific Name, Version null" { + # FindName() + $res = Find-PSResource -Name $testModuleName -Repository $localRepo + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0.0" + } + + It "should not find resource given nonexistant Name" { + $res = Find-PSResource -Name NonExistantModule -Repository $localRepo + $res | Should -BeNullOrEmpty + } + + # It "find resource(s) given wildcard Name" { + # # FindNameGlobbing + # $res = Find-PSResource -Name "test_local_*" -Repository $localRepo + # $res.Count | Should -BeGreaterThan 1 + # } + + $testCases2 = @{Version="[5.0.0.0]"; ExpectedVersions=@("5.0.0.0"); Reason="validate version, exact match"}, + @{Version="5.0.0.0"; ExpectedVersions=@("5.0.0.0"); Reason="validate version, exact match without bracket syntax"}, + @{Version="[1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0", "5.0.0.0"); Reason="validate version, exact range inclusive"}, + @{Version="(1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("3.0.0.0"); Reason="validate version, exact range exclusive"}, + @{Version="(1.0.0.0,)"; ExpectedVersions=@("3.0.0.0", "5.0.0.0"); Reason="validate version, minimum version exclusive"}, + @{Version="[1.0.0.0,)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0", "5.0.0.0"); Reason="validate version, minimum version inclusive"}, + @{Version="(,3.0.0.0)"; ExpectedVersions=@("1.0.0.0"); Reason="validate version, maximum version exclusive"}, + @{Version="(,3.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, maximum version inclusive"}, + @{Version="[1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} + @{Version="(1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("3.0.0.0", "5.0.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} + + It "find resource when given Name to " -TestCases $testCases2{ + # FindVersionGlobbing() + param($Version, $ExpectedVersions) + $res = Find-PSResource -Name $testModuleName -Version $Version -Repository $localRepo + foreach ($item in $res) { + $item.Name | Should -Be $testModuleName + $ExpectedVersions | Should -Contain $item.Version + } + } + + It "find all versions of resource when given specific Name, Version not null --> '*'" { + # FindVersionGlobbing() + $res = Find-PSResource -Name $testModuleName -Version "*" -Repository $localRepo + $res | ForEach-Object { + $_.Name | Should -Be $testModuleName + } + + $res.Count | Should -BeGreaterOrEqual 1 + } + + It "find resource with latest (including prerelease) version given Prerelease parameter" { + # FindName() + # test_module resource's latest version is a prerelease version, before that it has a non-prerelease version + $res = Find-PSResource -Name $testModuleName -Repository $localRepo + $res.Version | Should -Be "5.0.0.0" + + $resPrerelease = Find-PSResource -Name $testModuleName -Prerelease -Repository $localRepo + $resPrerelease.Version | Should -Be "5.2.5.0" + $resPrerelease.Prerelease | Should -Be "alpha001" + } + + It "find resources, including Prerelease version resources, when given Prerelease parameter" { + # FindVersionGlobbing() + $resWithoutPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $localRepo + $resWithPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $localRepo + $resWithPrerelease.Count | Should -BeGreaterOrEqual $resWithoutPrerelease.Count + } + + It "find resource that satisfies given Name and Tag property (single tag)" { + # FindNameWithTag() + $requiredTag = "test" + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $localRepo + $res.Name | Should -Be $testModuleName + $res.Tags | Should -Contain $requiredTag + } + + It "should not find resource if Name and Tag are not both satisfied (single tag)" { + # FindNameWithTag + $requiredTag = "Windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $localRepo + $res | Should -BeNullOrEmpty + } + + It "find resource that satisfies given Name and Tag property (multiple tags)" { + # FindNameWithTag() + $requiredTags = @("test", "Tag2") + $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $localRepo + $res.Name | Should -Be $testModuleName + $res.Tags | Should -Contain $requiredTags[0] + $res.Tags | Should -Contain $requiredTags[1] + } + + It "find all resources that satisfy Name pattern and have specified Tag (single tag)" { + # FindNameGlobbingWithTag() + $requiredTag = "test" + $nameWithWildcard = "test_local_mod*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTag -Repository $localRepo + $res.Count | Should -BeGreaterThan 1 + foreach ($pkg in $res) + { + $pkg.Name | Should -BeLike $nameWithWildcard + $pkg.Tags | Should -Contain $requiredTag + } + } + + It "should not find resources if both Name pattern and Tags are not satisfied (single tag)" { + # FindNameGlobbingWithTag() + $requiredTag = "windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name "test_module*" -Tag $requiredTag -Repository $localRepo + $res | Should -BeNullOrEmpty + } + + It "find all resources that satisfy Name pattern and have specified Tag (multiple tags)" { + # FindNameGlobbingWithTag() + $requiredTags = @("test", "Tag2") + $nameWithWildcard = "test_local_mod*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTags -Repository $localRepo + $res.Count | Should -BeGreaterThan 1 + foreach ($pkg in $res) + { + $pkg.Name | Should -BeLike $nameWithWildcard + $pkg.Tags | Should -Contain $requiredTags[0] + $pkg.Tags | Should -Contain $requiredTags[1] + } + } + + It "find resource that satisfies given Name, Version and Tag property (single tag)" { + # FindVersionWithTag() + $requiredTag = "test" + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $localRepo + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0.0" + $res.Tags | Should -Contain $requiredTag + } + + It "should not find resource if Name, Version and Tag property are not all satisfied (single tag)" { + # FindVersionWithTag() + $requiredTag = "windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $localRepo + $res | Should -BeNullOrEmpty + } + + It "find resource that satisfies given Name, Version and Tag property (multiple tags)" { + # FindVersionWithTag() + $requiredTags = @("test", "Tag2") + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $localRepo + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0.0" + $res.Tags | Should -Contain $requiredTags[0] + $res.Tags | Should -Contain $requiredTags[1] + + } + + It "find resource given CommandName" { + $res = Find-PSResource -CommandName $commandName -Repository $localRepo + foreach ($item in $res) { + $item.Names | Should -Be $commandName + $item.ParentResource.Includes.Command | Should -Contain $commandName + } + } + + It "find resource given DscResourceName" { + $res = Find-PSResource -DscResourceName $dscResourceName -Repository $localRepo + foreach ($item in $res) { + $item.Names | Should -Be $dscResourceName + $item.ParentResource.Includes.DscResource | Should -Contain $dscResourceName + } + } +} diff --git a/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 new file mode 100644 index 000000000..6d479fc95 --- /dev/null +++ b/test/FindPSResourceTests/FindPSResourceV2Server.Tests.ps1 @@ -0,0 +1,352 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Find-PSResource for V2 Server Protocol' { + + BeforeAll{ + $PSGalleryName = Get-PSGalleryName + $testModuleName = "test_module" + $testScriptName = "test_script" + $commandName = "Get-TargetResource" + $dscResourceName = "SystemLocale" + $parentModuleName = "SystemLocaleDsc" + Get-NewPSResourceRepositoryFile + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "find resource given specific Name, Version null" { + # FindName() + $res = Find-PSResource -Name $testModuleName -Repository $PSGalleryName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0.0" + } + + It "should not find resource given nonexistant Name" { + $res = Find-PSResource -Name NonExistantModule -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameResponseConversionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + $res | Should -BeNullOrEmpty + } + + It "find resource(s) given wildcard Name" { + # FindNameGlobbing + $foundScript = $False + $res = Find-PSResource -Name "test_*" -Repository $PSGalleryName + $res.Count | Should -BeGreaterThan 1 + # should find Module and Script resources + foreach ($item in $res) + { + if ($item.Type -eq "Script") + { + $foundScript = $true + } + } + + $foundScript | Should -BeTrue + } + + $testCases2 = @{Version="[5.0.0.0]"; ExpectedVersions=@("5.0.0.0"); Reason="validate version, exact match"}, + @{Version="5.0.0.0"; ExpectedVersions=@("5.0.0.0"); Reason="validate version, exact match without bracket syntax"}, + @{Version="[1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0", "5.0.0.0"); Reason="validate version, exact range inclusive"}, + @{Version="(1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("3.0.0.0"); Reason="validate version, exact range exclusive"}, + @{Version="(1.0.0.0,)"; ExpectedVersions=@("3.0.0.0", "5.0.0.0"); Reason="validate version, minimum version exclusive"}, + @{Version="[1.0.0.0,)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0", "5.0.0.0"); Reason="validate version, minimum version inclusive"}, + @{Version="(,3.0.0.0)"; ExpectedVersions=@("1.0.0.0"); Reason="validate version, maximum version exclusive"}, + @{Version="(,3.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, maximum version inclusive"}, + @{Version="[1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} + @{Version="(1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("3.0.0.0", "5.0.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} + + It "find resource when given Name to " -TestCases $testCases2{ + # FindVersionGlobbing() + param($Version, $ExpectedVersions) + $res = Find-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName + foreach ($item in $res) { + $item.Name | Should -Be $testModuleName + $ExpectedVersions | Should -Contain $item.Version + } + } + + It "find all versions of resource when given specific Name, Version not null --> '*'" { + # FindVersionGlobbing() + $res = Find-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName + $res | ForEach-Object { + $_.Name | Should -Be $testModuleName + } + + $res.Count | Should -BeGreaterOrEqual 1 + } + + It "find resource with latest (including prerelease) version given Prerelease parameter" { + # FindName() + # test_module resource's latest version is a prerelease version, before that it has a non-prerelease version + $res = Find-PSResource -Name $testModuleName -Repository $PSGalleryName + $res.Version | Should -Be "5.0.0.0" + + $resPrerelease = Find-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName + $resPrerelease.Version | Should -Be "5.2.5" + $resPrerelease.Prerelease | Should -Be "alpha001" + } + + It "find resources, including Prerelease version resources, when given Prerelease parameter" { + # FindVersionGlobbing() + $resWithoutPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName + $resWithPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName + $resWithPrerelease.Count | Should -BeGreaterOrEqual $resWithoutPrerelease.Count + } + + It "find resource and its dependency resources with IncludeDependencies parameter" { + # FindName() with deps + $resWithoutDependencies = Find-PSResource -Name "TestModuleWithDependencyE" -Repository $PSGalleryName + $resWithoutDependencies.Count | Should -Be 1 + $resWithoutDependencies.Name | Should -Be "TestModuleWithDependencyE" + + # TestModuleWithDependencyE has the following dependencies: + # TestModuleWithDependencyC <= 1.0.0.0 + # TestModuleWithDependencyB >= 1.0.0.0 + # TestModuleWithDependencyD <= 1.0.0.0 + + $resWithDependencies = Find-PSResource -Name "TestModuleWithDependencyE" -IncludeDependencies -Repository $PSGalleryName + $resWithDependencies.Count | Should -BeGreaterThan $resWithoutDependencies.Count + + $foundParentPkgE = $false + $foundDepB = $false + $foundDepBCorrectVersion = $false + $foundDepC = $false + $foundDepCCorrectVersion = $false + $foundDepD = $false + $foundDepDCorrectVersion = $false + foreach ($pkg in $resWithDependencies) + { + if ($pkg.Name -eq "TestModuleWithDependencyE") + { + $foundParentPkgE = $true + } + elseif ($pkg.Name -eq "TestModuleWithDependencyC") + { + $foundDepC = $true + $foundDepCCorrectVersion = [System.Version]$pkg.Version -le [System.Version]"1.0" + } + elseif ($pkg.Name -eq "TestModuleWithDependencyB") + { + $foundDepB = $true + $foundDepBCorrectVersion = [System.Version]$pkg.Version -ge [System.Version]"3.0" + } + elseif ($pkg.Name -eq "TestModuleWithDependencyD") + { + $foundDepD = $true + $foundDepDCorrectVersion = [System.Version]$pkg.Version -le [System.Version]"1.0" + } + } + + $foundParentPkgE | Should -Be $true + $foundDepC | Should -Be $true + $foundDepCCorrectVersion | Should -Be $true + $foundDepB | Should -Be $true + $foundDepBCorrectVersion | Should -Be $true + $foundDepD | Should -Be $true + $foundDepDCorrectVersion | Should -Be $true + } + + It "find resource of Type script or module from PSGallery, when no Type parameter provided" { + # FindName() script + $resScript = Find-PSResource -Name $testScriptName -Repository $PSGalleryName + $resScript.Name | Should -Be $testScriptName + $resScriptType = Out-String -InputObject $resScript.Type + $resScriptType.Replace(",", " ").Split() | Should -Contain "Script" + + $resModule = Find-PSResource -Name $testModuleName -Repository $PSGalleryName + $resModule.Name | Should -Be $testModuleName + $resModuleType = Out-String -InputObject $resModule.Type + $resModuleType.Replace(",", " ").Split() | Should -Contain "Module" + } + + It "find resource of Type Script from PSGallery, when Type Script specified" { + # FindName() Type script + $resScript = Find-PSResource -Name $testScriptName -Repository $PSGalleryName -Type "Script" + $resScript.Name | Should -Be $testScriptName + $resScriptType = Out-String -InputObject $resScript.Type + $resScriptType.Replace(",", " ").Split() | Should -Contain "Script" + } + + It "find all resources of Type Module when Type parameter set is used" { + $foundScript = $False + $res = Find-PSResource -Name "test*" -Type Module -Repository $PSGalleryName + $res.Count | Should -BeGreaterThan 1 + foreach ($item in $res) { + if ($item.Type -eq "Script") + { + $foundScript = $True + } + } + + $foundScript | Should -Be $False + } + + # It "find resources only with Tag parameter" { + # $resWithEitherExpectedTag = @("NetworkingDsc", "DSCR_FileContent", "SS.PowerShell") + # $res = Find-PSResource -Name "NetworkingDsc", "HPCMSL", "DSCR_FileContent", "SS.PowerShell", "PowerShellGet" -Tag "Dsc", "json" -Repository $PSGalleryName + # foreach ($item in $res) { + # $resWithEitherExpectedTag | Should -Contain $item.Name + # } + # } + + It "find resource that satisfies given Name and Tag property (single tag)" { + # FindNameWithTag() + $requiredTag = "test" + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $PSGalleryName + $res.Name | Should -Be $testModuleName + $res.Tags | Should -Contain $requiredTag + } + + It "should not find resource if Name and Tag are not both satisfied (single tag)" { + # FindNameWithTag + $requiredTag = "Windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameResponseConversionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "find resource that satisfies given Name and Tag property (multiple tags)" { + # FindNameWithTag() + $requiredTags = @("test", "Tag2") + $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $PSGalleryName + $res.Name | Should -Be $testModuleName + $res.Tags | Should -Contain $requiredTags[0] + $res.Tags | Should -Contain $requiredTags[1] + } + + It "should not find resource if Name and Tag are not both satisfied (multiple tag)" { + # FindNameWithTag + $requiredTags = @("test", "Windows") # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameResponseConversionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "find all resources that satisfy Name pattern and have specified Tag (single tag)" { + # FindNameGlobbingWithTag() + $requiredTag = "test" + $nameWithWildcard = "test_module*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTag -Repository $PSGalleryName + $res.Count | Should -BeGreaterThan 1 + foreach ($pkg in $res) + { + $pkg.Name | Should -BeLike $nameWithWildcard + $pkg.Tags | Should -Contain $requiredTag + } + } + + It "should not find resources if both Name pattern and Tags are not satisfied (single tag)" { + # FindNameGlobbingWithTag() + $requiredTag = "windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name "test_module*" -Tag $requiredTag -Repository $PSGalleryName + $res | Should -BeNullOrEmpty + } + + It "find all resources that satisfy Name pattern and have specified Tag (multiple tags)" { + # FindNameGlobbingWithTag() + $requiredTags = @("test", "Tag2") + $nameWithWildcard = "test_module*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTags -Repository $PSGalleryName + $res.Count | Should -BeGreaterThan 1 + foreach ($pkg in $res) + { + $pkg.Name | Should -BeLike $nameWithWildcard + $pkg.Tags | Should -Contain $requiredTags[0] + $pkg.Tags | Should -Contain $requiredTags[1] + } + } + + It "should not find resources if both Name pattern and Tags are not satisfied (multiple tags)" { + # FindNameGlobbingWithTag() # tag "windows" is not present for test_module package + $requiredTags = @("test", "windows") + $res = Find-PSResource -Name "test_module*" -Tag $requiredTags -Repository $PSGalleryName + $res | Should -BeNullOrEmpty + } + + It "find resource that satisfies given Name, Version and Tag property (single tag)" { + # FindVersionWithTag() + $requiredTag = "test" + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $PSGalleryName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0.0" + $res.Tags | Should -Contain $requiredTag + } + + It "should not find resource if Name, Version and Tag property are not all satisfied (single tag)" { + # FindVersionWithTag() + $requiredTag = "windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindVersionResponseConversionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "find resource that satisfies given Name, Version and Tag property (multiple tags)" { + # FindVersionWithTag() + $requiredTags = @("test", "Tag2") + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $PSGalleryName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0.0" + $res.Tags | Should -Contain $requiredTags[0] + $res.Tags | Should -Contain $requiredTags[1] + + } + + It "should not find resource if Name, Version and Tag property are not all satisfied (multiple tags)" { + # FindVersionWithTag() + $requiredTags = @("test", "windows") + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindVersionResponseConversionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + # It "find all resources with specified tag given Tag property" { + # # FindTag() + # $foundTestModule = $False + # $foundTestScript = $False + # $tagToFind = "Tag2" + # $res = Find-PSResource -Tag $tagToFind -Repository $PSGalleryName + # foreach ($item in $res) { + # $item.Tags -contains $tagToFind | Should -Be $True + + # if ($item.Name -eq $testModuleName) + # { + # $foundTestModule = $True + # } + + # if ($item.Name -eq $testScriptName) + # { + # $foundTestScript = $True + # } + # } + + # $foundTestModule | Should -Be $True + # $foundTestScript | Should -Be $True + # } + + It "find resource given CommandName" { + $res = Find-PSResource -CommandName $commandName -Repository $PSGalleryName + foreach ($item in $res) { + $item.Names | Should -Be $commandName + $item.ParentResource.Includes.Command | Should -Contain $commandName + } + } + + It "find resource given DscResourceName" { + $res = Find-PSResource -DscResourceName $dscResourceName -Repository $PSGalleryName + foreach ($item in $res) { + $item.Names | Should -Be $dscResourceName + $item.ParentResource.Includes.DscResource | Should -Contain $dscResourceName + } + } +} diff --git a/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 new file mode 100644 index 000000000..b87768784 --- /dev/null +++ b/test/FindPSResourceTests/FindPSResourceV3Server.Tests.ps1 @@ -0,0 +1,267 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Find-PSResource for V2 Server Protocol' { + + BeforeAll{ + $NuGetGalleryName = Get-NuGetGalleryName + $testModuleName = "test_module" + Get-NewPSResourceRepositoryFile + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "find resource given specific Name, Version null" { + # FindName() + $res = Find-PSResource -Name $testModuleName -Repository $NuGetGalleryName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0" + } + + It "should not find resource given nonexistant Name" { + $res = Find-PSResource -Name NonExistantModule -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + $res | Should -BeNullOrEmpty + } + + It "find resource(s) given wildcard Name" { + # FindNameGlobbing + $wildcardName = "test_module*" + $res = Find-PSResource -Name $wildcardName -Repository $NuGetGalleryName + $res.Count | Should -BeGreaterThan 1 + foreach ($item in $res) + { + $item.Name | Should -BeLike $wildcardName + } + } + + $testCases2 = @{Version="[5.0.0.0]"; ExpectedVersions=@("5.0.0"); Reason="validate version, exact match"}, + @{Version="5.0.0.0"; ExpectedVersions=@("5.0.0"); Reason="validate version, exact match without bracket syntax"}, + @{Version="[1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, exact range inclusive"}, + @{Version="(1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("3.0.0"); Reason="validate version, exact range exclusive"}, + @{Version="(1.0.0.0,)"; ExpectedVersions=@("3.0.0", "5.0.0"); Reason="validate version, minimum version exclusive"}, + @{Version="[1.0.0.0,)"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, minimum version inclusive"}, + @{Version="(,3.0.0.0)"; ExpectedVersions=@("1.0.0"); Reason="validate version, maximum version exclusive"}, + @{Version="(,3.0.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, maximum version inclusive"}, + @{Version="[1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} + @{Version="(1.0.0.0, 5.0.0.0]"; ExpectedVersions=@("3.0.0", "5.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} + + It "find resource when given Name to " -TestCases $testCases2{ + # FindVersionGlobbing() + param($Version, $ExpectedVersions) + $res = Find-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName + foreach ($item in $res) { + $item.Name | Should -Be $testModuleName + $ExpectedVersions | Should -Contain $item.Version + } + } + + It "find all versions of resource when given specific Name, Version not null --> '*'" { + # FindVersionGlobbing() + $res = Find-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName + $res | ForEach-Object { + $_.Name | Should -Be $testModuleName + } + + $res.Count | Should -BeGreaterOrEqual 1 + } + + It "find resource with latest (including prerelease) version given Prerelease parameter" { + # FindName() + # test_module resource's latest version is a prerelease version, before that it has a non-prerelease version + $res = Find-PSResource -Name $testModuleName -Repository $NuGetGalleryName + $res.Version | Should -Be "5.0.0" + + $resPrerelease = Find-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName + $resPrerelease.Version | Should -Be "5.2.5" + $resPrerelease.Prerelease | Should -Be "alpha001" + } + + It "find resources, including Prerelease version resources, when given Prerelease parameter" { + # FindVersionGlobbing() + $resWithoutPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName + $resWithPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName + $resWithPrerelease.Count | Should -BeGreaterOrEqual $resWithoutPrerelease.Count + } + + It "find resource and its dependency resources with IncludeDependencies parameter" { + # find with dependencies is not yet supported for V3, so this should only install parent package + $pkg = Find-PSResource -Name "TestModuleWithDependencyE" -IncludeDependencies -Repository $NuGetGalleryName + $pkg.Count | Should -Be 1 + $pkg.Name | Should -Be "TestModuleWithDependencyE" + } + + # It "find resources only with Tag parameter" { + # $resWithEitherExpectedTag = @("NetworkingDsc", "DSCR_FileContent", "SS.PowerShell") + # $res = Find-PSResource -Name "NetworkingDsc", "HPCMSL", "DSCR_FileContent", "SS.PowerShell", "PowerShellGet" -Tag "Dsc", "json" -Repository $NuGetGalleryName + # foreach ($item in $res) { + # $resWithEitherExpectedTag | Should -Contain $item.Name + # } + # } + + It "find resource that satisfies given Name and Tag property (single tag)" { + # FindNameWithTag() + $requiredTag = "test" + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $NuGetGalleryName + $res.Name | Should -Be $testModuleName + $res.Tags | Should -Contain $requiredTag + } + + It "should not find resource if Name and Tag are not both satisfied (single tag)" { + # FindNameWithTag + $requiredTag = "Windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "find resource that satisfies given Name and Tag property (multiple tags)" { + # FindNameWithTag() + $requiredTags = @("test", "Tag2") + $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $NuGetGalleryName + $res.Name | Should -Be $testModuleName + $res.Tags | Should -Contain $requiredTags[0] + $res.Tags | Should -Contain $requiredTags[1] + } + + It "should not find resource if Name and Tag are not both satisfied (multiple tag)" { + # FindNameWithTag + $requiredTags = @("test", "Windows") # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "find all resources that satisfy Name pattern and have specified Tag (single tag)" { + # FindNameGlobbingWithTag() + $requiredTag = "test" + $nameWithWildcard = "test_module*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTag -Repository $NuGetGalleryName + $res.Count | Should -BeGreaterThan 1 + foreach ($pkg in $res) + { + $pkg.Name | Should -BeLike $nameWithWildcard + $pkg.Tags | Should -Contain $requiredTag + } + } + + It "should not find resources if both Name pattern and Tags are not satisfied (single tag)" { + # FindNameGlobbingWithTag() + $requiredTag = "windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name "test_module*" -Tag $requiredTag -Repository $NuGetGalleryName + $res | Should -BeNullOrEmpty + } + + It "find all resources that satisfy Name pattern and have specified Tag (multiple tags)" { + # FindNameGlobbingWithTag() + $requiredTags = @("test", "Tag2") + $nameWithWildcard = "test_module*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTags -Repository $NuGetGalleryName + $res.Count | Should -BeGreaterThan 1 + foreach ($pkg in $res) + { + $pkg.Name | Should -BeLike $nameWithWildcard + $pkg.Tags | Should -Contain $requiredTags[0] + $pkg.Tags | Should -Contain $requiredTags[1] + } + } + + It "should not find resources if both Name pattern and Tags are not satisfied (multiple tags)" { + # FindNameGlobbingWithTag() # tag "windows" is not present for test_module package + $requiredTags = @("test", "windows") + $res = Find-PSResource -Name "test_module*" -Tag $requiredTags -Repository $NuGetGalleryName + $res | Should -BeNullOrEmpty + } + + It "find resource that satisfies given Name, Version and Tag property (single tag)" { + # FindVersionWithTag() + $requiredTag = "test" + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $NuGetGalleryName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0" + $res.Tags | Should -Contain $requiredTag + } + + It "should not find resource if Name, Version and Tag property are not all satisfied (single tag)" { + # FindVersionWithTag() + $requiredTag = "windows" # tag "windows" is not present for test_module package + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindVersionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "find resource that satisfies given Name, Version and Tag property (multiple tags)" { + # FindVersionWithTag() + $requiredTags = @("test", "Tag2") + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $NuGetGalleryName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0" + $res.Tags | Should -Contain $requiredTags[0] + $res.Tags | Should -Contain $requiredTags[1] + + } + + It "should not find resource if Name, Version and Tag property are not all satisfied (multiple tags)" { + # FindVersionWithTag() + $requiredTags = @("test", "windows") + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTags -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindVersionFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + # It "find all resources with specified tag given Tag property" { + # # FindTag() + # $foundTestModule = $False + # $foundTestScript = $False + # $tagToFind = "Tag2" + # $res = Find-PSResource -Tag $tagToFind -Repository $NuGetGalleryName + # foreach ($item in $res) { + # $item.Tags -contains $tagToFind | Should -Be $True + + # if ($item.Name -eq $testModuleName) + # { + # $foundTestModule = $True + # } + + # if ($item.Name -eq $testScriptName) + # { + # $foundTestScript = $True + # } + # } + + # $foundTestModule | Should -Be $True + # $foundTestScript | Should -Be $True + # } + + It "should not find resource given CommandName" { + $res = Find-PSResource -CommandName "command" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDSCResourceFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "should not find resource given DscResourceName" { + $res = Find-PSResource -DscResourceName "dscResource" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDSCResourceFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + } + + It "should not find all resources given Name '*'" { + $res = Find-PSResource -Name "*" -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFail,Microsoft.PowerShell.PowerShellGet.Cmdlets.FindPSResource" + + } +} diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResource.Tests.ps1 similarity index 96% rename from test/InstallPSResource.Tests.ps1 rename to test/InstallPSResourceTests/InstallPSResource.Tests.ps1 index e6106c869..a744d8e4b 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResource.Tests.ps1 @@ -1,570 +1,562 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -$ProgressPreference = "SilentlyContinue" -Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force - -Describe 'Test Install-PSResource for Module' { - - BeforeAll { - $PSGalleryName = Get-PSGalleryName - $PSGalleryUri = Get-PSGalleryLocation - $NuGetGalleryName = Get-NuGetGalleryName - $testModuleName = "test_module" - $testModuleName2 = "TestModule99" - $testScriptName = "test_script" - $PackageManagement = "PackageManagement" - $RequiredResourceJSONFileName = "TestRequiredResourceFile.json" - $RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1" - Get-NewPSResourceRepositoryFile - Register-LocalRepos - } - - AfterEach { - Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue - } - - AfterAll { - Get-RevertPSResourceRepositoryFile - } - - $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, - @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, - @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} - - It "Should not install resource with wildcard in name" -TestCases $testCases { - param($Name, $ErrorId) - Install-PSResource -Name $Name -ErrorVariable err -ErrorAction SilentlyContinue - $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - } - - It "Install specific module resource by name" { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0.0" - } - - It "Install specific script resource by name" { - Install-PSResource -Name $testScriptName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testScriptName - $pkg.Name | Should -Be $testScriptName - $pkg.Version | Should -Be "3.5.0.0" - } - - It "Install multiple resources by name" { - $pkgNames = @($testModuleName,$testModuleName2) - Install-PSResource -Name $pkgNames -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $pkgNames - $pkg.Name | Should -Be $pkgNames - } - - It "Should not install resource given nonexistant name" { - Install-PSResource -Name "NonExistantModule" -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue - $pkg = Get-PSResource "NonExistantModule" - $pkg.Name | Should -BeNullOrEmpty - $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "ResourceNotFoundError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - } - - # Do some version testing, but Find-PSResource should be doing thorough testing - It "Should install resource given name and exact version" { - Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "1.0.0.0" - } - - It "Should install resource given name and exact version with bracket syntax" { - Install-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "1.0.0.0" - } - - It "Should install resource given name and exact range inclusive [1.0.0, 5.0.0]" { - Install-PSResource -Name $testModuleName -Version "[1.0.0, 5.0.0]" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0.0" - } - - It "Should install resource given name and exact range exclusive (1.0.0, 5.0.0)" { - Install-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "3.0.0.0" - } - - # TODO: Update this test and others like it that use try/catch blocks instead of Should -Throw - It "Should not install resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { - $Version = "(1.0.0.0)" - try { - Install-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue - } - catch - {} - $Error[0].FullyQualifiedErrorId | Should -be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - - $res = Get-PSResource $testModuleName - $res | Should -BeNullOrEmpty - } - - It "Should not install resource with incorrectly formatted version such as version formatted with invalid delimiter [1-0-0-0]" { - $Version="[1-0-0-0]" - try { - Install-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue - } - catch - {} - $Error[0].FullyQualifiedErrorId | Should -be "ResourceNotFoundError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - - $res = Get-PSResource $testModuleName - $res | Should -BeNullOrEmpty - } - - It "Install resource when given Name, Version '*', should install the latest version" { - Install-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0.0" - } - - It "Install resource with latest (including prerelease) version given Prerelease parameter" { - Install-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.2.5" - $pkg.Prerelease | Should -Be "alpha001" - } - - It "Install a module with a dependency" { - Uninstall-PSResource -Name "TestModuleWithDependency*" -Version "*" -SkipDependencyCheck - Install-PSResource -Name "TestModuleWithDependencyC" -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository - - $pkg = Get-PSResource "TestModuleWithDependencyC" - $pkg.Name | Should -Be "TestModuleWithDependencyC" - $pkg.Version | Should -Be "1.0.0.0" - - $pkg = Get-PSResource "TestModuleWithDependencyB" - $pkg.Name | Should -Be "TestModuleWithDependencyB" - $pkg.Version | Should -Be "3.0.0.0" - - $pkg = Get-PSResource "TestModuleWithDependencyD" - $pkg.Name | Should -Be "TestModuleWithDependencyD" - $pkg.Version | Should -Be "1.0.0.0" - } - - It "Install a module with a dependency and skip installing the dependency" { - Uninstall-PSResource -Name "TestModuleWithDependency*" -Version "*" -SkipDependencyCheck - Install-PSResource -Name "TestModuleWithDependencyC" -Version "1.0.0.0" -SkipDependencyCheck -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource "TestModuleWithDependencyC" - $pkg.Name | Should -Be "TestModuleWithDependencyC" - $pkg.Version | Should -Be "1.0.0.0" - - $pkg = Get-PSResource "TestModuleWithDependencyB", "TestModuleWithDependencyD" - $pkg | Should -BeNullOrEmpty - } - - It "Install resource via InputObject by piping from Find-PSresource" { - Find-PSResource -Name $testModuleName -Repository $PSGalleryName | Install-PSResource -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0.0" - } - - It "Install resource under specified in PSModulePath" { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - ($env:PSModulePath).Contains($pkg.InstalledLocation) - } - - It "Install resource with companyname, copyright and repository source location and validate" { - Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository PSGallery -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Version | Should -Be "5.2.5" - $pkg.Prerelease | Should -Be "alpha001" - - $pkg.CompanyName | Should -Be "Anam" - $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." - $pkg.RepositorySourceLocation | Should -Be $PSGalleryUri - } - - - It "Install script with companyname, copyright, and repository source location and validate" { - Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - - $res = Get-PSResource "Install-VSCode" -Version "1.4.2" - $res.Name | Should -Be "Install-VSCode" - $res.Version | Should -Be "1.4.2.0" - $res.CompanyName | Should -Be "Microsoft Corporation" - $res.Copyright | Should -Be "(c) Microsoft Corporation" - $res.RepositorySourceLocation | Should -Be $PSGalleryUri - } - - # Windows only - It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true - } - - # Windows only - It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { - Install-PSResource -Name "testmodule99" -Repository $PSGalleryName -TrustRepository -Scope AllUsers -Verbose - $pkg = Get-Module "testmodule99" -ListAvailable - $pkg.Name | Should -Be "testmodule99" - $pkg.Path.ToString().Contains("Program Files") - } - - # Windows only - It "Install resource under no specified scope - Windows only" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true - } - - # Unix only - # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' - It "Install resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true - } - - # Unix only - # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' - It "Install resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true - } - - It "Should not install resource that is already installed" { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -WarningVariable WarningVar -warningaction SilentlyContinue - $WarningVar | Should -Not -BeNullOrEmpty - } - - It "Reinstall resource that is already installed with -Reinstall parameter" { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0.0" - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -Reinstall -TrustRepository - $pkg = Get-PSResource $testModuleName - $pkg.Name | Should -Be $testModuleName - $pkg.Version | Should -Be "5.0.0.0" - } - - It "Restore resource after reinstall fails" { - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository - $pkg = Get-Module $testModuleName -ListAvailable - $pkg.Name | Should -Contain $testModuleName - $pkg.Version | Should -Contain "5.0.0.0" - - $resourcePath = Split-Path -Path $pkg.Path -Parent - $resourceFiles = Get-ChildItem -Path $resourcePath -Recurse - - # Lock resource file to prevent reinstall from succeeding. - $fs = [System.IO.File]::Open($resourceFiles[0].FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) - try - { - # Reinstall of resource should fail with one of its files locked. - Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Reinstall -ErrorVariable ev -ErrorAction Silent - $ev.FullyQualifiedErrorId | Should -BeExactly 'InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource' - } - finally - { - $fs.Close() - } - - # Verify that resource module has been restored. - (Get-ChildItem -Path $resourcePath -Recurse).Count | Should -BeExactly $resourceFiles.Count - } - - # It "Install resource that requires accept license with -AcceptLicense flag" { - # Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName -AcceptLicense - # $pkg = Get-PSResource "testModuleWithlicense" - # $pkg.Name | Should -Be "testModuleWithlicense" - # $pkg.Version | Should -Be "0.0.3.0" - # } - - - It "Install resource with cmdlet names from a module already installed (should clobber)" { - Install-PSResource -Name "CLobberTestModule1" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource "ClobberTestModule1" - $pkg.Name | Should -Be "ClobberTestModule1" - $pkg.Version | Should -Be "0.0.1.0" - - Install-PSResource -Name "ClobberTestModule2" -Repository $PSGalleryName -TrustRepository - $pkg = Get-PSResource "ClobberTestModule2" - $pkg.Name | Should -Be "ClobberTestModule2" - $pkg.Version | Should -Be "0.0.1.0" - } - - It "Install resource from local repository given Repository parameter" { - $publishModuleName = "TestFindModule" - $repoName = "psgettestlocal" - Get-ModuleResourcePublishedToLocalRepoTestDrive $publishModuleName $repoName - Set-PSResourceRepository "psgettestlocal" -Trusted:$true - - Install-PSResource -Name $publishModuleName -Repository $repoName - $pkg = Get-PSResource $publishModuleName - $pkg | Should -Not -BeNullOrEmpty - $pkg.Name | Should -Be $publishModuleName - } - - It "Install module using -WhatIf, should not install the module" { - Install-PSResource -Name $testModuleName -WhatIf - - $res = Get-PSResource $testModuleName - $res | Should -BeNullOrEmpty - } - - It "Validates that a module with module-name script files (like Pester) installs under Modules path" { - - Install-PSResource -Name "testModuleWithScript" -Repository $PSGalleryName -TrustRepository - - $res = Get-PSResource "testModuleWithScript" - $res.InstalledLocation.ToString().Contains("Modules") | Should -Be $true - } - - It "Install module using -NoClobber, should throw clobber error and not install the module" { - Install-PSResource -Name "ClobberTestModule1" -Repository $PSGalleryName -TrustRepository - - $res = Get-PSResource "ClobberTestModule1" - $res.Name | Should -Be "ClobberTestModule1" - - Install-PSResource -Name "ClobberTestModule2" -Repository $PSGalleryName -TrustRepository -NoClobber -ErrorAction SilentlyContinue - $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - } - - It "Install PSResourceInfo object piped in" { - Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName | Install-PSResource -TrustRepository - $res = Get-PSResource -Name $testModuleName - $res.Name | Should -Be $testModuleName - $res.Version | Should -Be "1.0.0.0" - } - - It "Install module using -PassThru" { - $res = Install-PSResource -Name $testModuleName -Repository $PSGalleryName -PassThru -TrustRepository - $res.Name | Should -Contain $testModuleName - } - - It "Install modules using -RequiredResource with hashtable" { - $rrHash = @{ - test_module = @{ - version = "[1.0.0,5.0.0)" - repository = $PSGalleryName - } - - test_module2 = @{ - version = "[1.0.0,3.0.0)" - repository = $PSGalleryName - prerelease = "true" - } - - TestModule99 = @{} - } - - Install-PSResource -RequiredResource $rrHash -TrustRepository - - $res1 = Get-PSResource $testModuleName - $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0.0" - - $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" - $res2.Name | Should -Be "test_module2" - $res2.Version | Should -Be "2.5.0" - $res2.Prerelease | Should -Be "beta" - - $res3 = Get-PSResource $testModuleName2 - $res3.Name | Should -Be $testModuleName2 - $res3.Version | Should -Be "0.0.93.0" - } - - It "Install modules using -RequiredResource with JSON string" { - $rrJSON = "{ - 'test_module': { - 'version': '[1.0.0,5.0.0)', - 'repository': 'PSGallery' - }, - 'test_module2': { - 'version': '[1.0.0,3.0.0)', - 'repository': 'PSGallery', - 'prerelease': 'true' - }, - 'TestModule99': { - 'repository': 'PSGallery' - } - }" - - Install-PSResource -RequiredResource $rrJSON -TrustRepository - - $res1 = Get-PSResource $testModuleName - $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0.0" - - $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" - $res2.Name | Should -Be "test_module2" - $res2.Version | Should -Be "2.5.0" - $res2.Prerelease | Should -Be "beta" - - $res3 = Get-PSResource $testModuleName2 - $res3.Name | Should -Be $testModuleName2 - $res3.Version | Should -Be "0.0.93.0" - } - - It "Install modules using -RequiredResourceFile with PSD1 file" { - $rrFilePSD1 = Join-Path -Path $psscriptroot -ChildPath $RequiredResourcePSD1FileName - - Install-PSResource -RequiredResourceFile $rrFilePSD1 -TrustRepository - - $res1 = Get-PSResource $testModuleName - $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0.0" - - $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" - $res2.Name | Should -Be "test_module2" - $res2.Version | Should -Be "2.5.0" - $res2.Prerelease | Should -Be "beta" - - $res3 = Get-PSResource $testModuleName2 - $res3.Name | Should -Be $testModuleName2 - $res3.Version | Should -Be "0.0.93.0" - } - - It "Install modules using -RequiredResourceFile with JSON file" { - $rrFileJSON = Join-Path -Path $psscriptroot -ChildPath $RequiredResourceJSONFileName - - Install-PSResource -RequiredResourceFile $rrFileJSON -TrustRepository - - $res1 = Get-PSResource $testModuleName - $res1.Name | Should -Be $testModuleName - $res1.Version | Should -Be "3.0.0.0" - - $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" - $res2.Name | Should -Be "test_module2" - $res2.Version | Should -Be "2.5.0" - $res2.Prerelease | Should -Be "beta" - - $res3 = Get-PSResource $testModuleName2 - $res3.Name | Should -Be $testModuleName2 - $res3.Version | Should -Be "0.0.93.0" - } - - # Install module 1.4.3 (is authenticode signed and has catalog file) - # Should install successfully - It "Install modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - - $res1 = Get-PSResource $PackageManagement -Version "1.4.3" - $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.3.0" - } - - # Install module 1.4.7 (is authenticode signed and has no catalog file) - # Should not install successfully - It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - - $res1 = Get-PSResource $PackageManagement -Version "1.4.7" - $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.7.0" - } - - # Install module that is not authenticode signed - # Should FAIL to install the module - It "Install module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { - { Install-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - } - - # Install 1.4.4.1 (with incorrect catalog file) - # Should FAIL to install the module - It "Install module with incorrect catalog file" -Skip:(!(Get-IsWindows)) { - { Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "TestFileCatalogError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - } - - # Install script that is signed - # Should install successfully - It "Install script that is authenticode signed" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository - - $res1 = Get-PSResource "Install-VSCode" -Version "1.4.2" - $res1.Name | Should -Be "Install-VSCode" - $res1.Version | Should -Be "1.4.2.0" - } - - # Install script that is not signed - # Should throw - It "Install script that is not signed" -Skip:(!(Get-IsWindows)) { - { Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" - } - - It "Install module with -NoClobber parameter" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $TestModuleName -Version "5.0.0" -Repository $PSGalleryName -NoClobber -Reinstall -TrustRepository - - $res = Get-PSResource $TestModuleName - $res.Name | Should -Be $TestModuleName - $res.Version | Should -Be "5.0.0.0" - } -} - -<# Temporarily commented until -Tag is implemented for this Describe block -Describe 'Test Install-PSResource for interactive and root user scenarios' { - - BeforeAll{ - $TestGalleryName = Get-PoshTestGalleryName - $PSGalleryName = Get-PSGalleryName - $NuGetGalleryName = Get-NuGetGalleryName - Get-NewPSResourceRepositoryFile - Register-LocalRepos - } - - AfterEach { - Uninstall-PSResource "TestModule", "testModuleWithlicense" -SkipDependencyCheck -ErrorAction SilentlyContinue - } - - AfterAll { - Get-RevertPSResourceRepositoryFile - } - - # Unix only manual test - # Expected path should be similar to: '/usr/local/share/powershell/Modules' - It "Install resource under AllUsers scope - Unix only" -Skip:(Get-IsWindows) { - Install-PSResource -Name "TestModule" -Repository $TestGalleryName -Scope AllUsers - $pkg = Get-Module "TestModule" -ListAvailable - $pkg.Name | Should -Be "TestModule" - $pkg.Path.Contains("/usr/") | Should -Be $true - } - - # This needs to be manually tested due to prompt - It "Install resource that requires accept license without -AcceptLicense flag" { - Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName - $pkg = Get-PSResource "testModuleWithlicense" - $pkg.Name | Should -Be "testModuleWithlicense" - $pkg.Version | Should -Be "0.0.1.0" - } - - # This needs to be manually tested due to prompt - It "Install resource should prompt 'trust repository' if repository is not trusted" { - Set-PSResourceRepository PoshTestGallery -Trusted:$false - - Install-PSResource -Name "TestModule" -Repository $TestGalleryName -confirm:$false - - $pkg = Get-Module "TestModule" -ListAvailable - $pkg.Name | Should -Be "TestModule" - - Set-PSResourceRepository PoshTestGallery -Trusted - } -} -#> +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force + +Describe 'Test Install-PSResource for Module' { + + BeforeAll { + $PSGalleryName = Get-PSGalleryName + $PSGalleryUri = Get-PSGalleryLocation + $NuGetGalleryName = Get-NuGetGalleryName + $testModuleName = "test_module" + $testModuleName2 = "TestModule99" + $testScriptName = "test_script" + $PackageManagement = "PackageManagement" + $RequiredResourceJSONFileName = "TestRequiredResourceFile.json" + $RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, + @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, + @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} + + It "Should not install resource with wildcard in name" -TestCases $testCases { + param($Name, $ErrorId) + Install-PSResource -Name $Name -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + It "Install specific module resource by name" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Install specific script resource by name" { + Install-PSResource -Name $testScriptName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testScriptName + $pkg.Name | Should -Be $testScriptName + $pkg.Version | Should -Be "3.5.0.0" + } + + It "Install multiple resources by name" { + $pkgNames = @($testModuleName,$testModuleName2) + Install-PSResource -Name $pkgNames -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $pkgNames + $pkg.Name | Should -Be $pkgNames + } + + It "Should not install resource given nonexistant name" { + Install-PSResource -Name "NonExistantModule" -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $pkg = Get-PSResource "NonExistantModule" + $pkg.Name | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "ResourceNotFoundError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # Do some version testing, but Find-PSResource should be doing thorough testing + It "Should install resource given name and exact version" { + Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "1.0.0.0" + } + + It "Should install resource given name and exact version with bracket syntax" { + Install-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "1.0.0.0" + } + + It "Should install resource given name and exact range inclusive [1.0.0, 5.0.0]" { + Install-PSResource -Name $testModuleName -Version "[1.0.0, 5.0.0]" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Should install resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Install-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "3.0.0.0" + } + + # TODO: Update this test and others like it that use try/catch blocks instead of Should -Throw + It "Should not install resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version = "(1.0.0.0)" + try { + Install-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + } + catch + {} + $Error[0].FullyQualifiedErrorId | Should -be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + + $res = Get-PSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Should not install resource with incorrectly formatted version such as version formatted with invalid delimiter [1-0-0-0]" { + $Version="[1-0-0-0]" + try { + Install-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + } + catch + {} + $Error[0].FullyQualifiedErrorId | Should -be "ResourceNotFoundError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + + $res = Get-PSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Install resource when given Name, Version '*', should install the latest version" { + Install-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Install resource with latest (including prerelease) version given Prerelease parameter" { + Install-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + } + + It "Install a module with a dependency" { + Uninstall-PSResource -Name "TestModuleWithDependency*" -Version "*" -SkipDependencyCheck + Install-PSResource -Name "TestModuleWithDependencyC" -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository + + $pkg = Get-PSResource "TestModuleWithDependencyC" + $pkg.Name | Should -Be "TestModuleWithDependencyC" + $pkg.Version | Should -Be "1.0.0.0" + + $pkg = Get-PSResource "TestModuleWithDependencyB" + $pkg.Name | Should -Be "TestModuleWithDependencyB" + $pkg.Version | Should -Be "3.0.0.0" + + $pkg = Get-PSResource "TestModuleWithDependencyD" + $pkg.Name | Should -Be "TestModuleWithDependencyD" + $pkg.Version | Should -Be "1.0.0.0" + } + + It "Install a module with a dependency and skip installing the dependency" { + Uninstall-PSResource -Name "TestModuleWithDependency*" -Version "*" -SkipDependencyCheck + Install-PSResource -Name "TestModuleWithDependencyC" -Version "1.0.0.0" -SkipDependencyCheck -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource "TestModuleWithDependencyC" + $pkg.Name | Should -Be "TestModuleWithDependencyC" + $pkg.Version | Should -Be "1.0.0.0" + + $pkg = Get-PSResource "TestModuleWithDependencyB", "TestModuleWithDependencyD" + $pkg | Should -BeNullOrEmpty + } + + It "Install resource via InputObject by piping from Find-PSresource" { + Find-PSResource -Name $testModuleName -Repository $PSGalleryName | Install-PSResource -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Install resource under specified in PSModulePath" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + ($env:PSModulePath).Contains($pkg.InstalledLocation) + } + + It "Install resource with companyname, copyright and repository source location and validate" { + Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository PSGallery -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + + $pkg.CompanyName | Should -Be "Anam" + $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." + $pkg.RepositorySourceLocation | Should -Be $PSGalleryUri + } + + + It "Install script with companyname, copyright, and repository source location and validate" { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource "Install-VSCode" -Version "1.4.2" + $res.Name | Should -Be "Install-VSCode" + $res.Version | Should -Be "1.4.2.0" + $res.CompanyName | Should -Be "Microsoft Corporation" + $res.Copyright | Should -Be "(c) Microsoft Corporation" + $res.RepositorySourceLocation | Should -Be $PSGalleryUri + } + + # Windows only + It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + } + + # Windows only + It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { + Install-PSResource -Name "testmodule99" -Repository $PSGalleryName -TrustRepository -Scope AllUsers -Verbose + $pkg = Get-Module "testmodule99" -ListAvailable + $pkg.Name | Should -Be "testmodule99" + $pkg.Path.ToString().Contains("Program Files") + } + + # Windows only + It "Install resource under no specified scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Install resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Install resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true + } + + It "Should not install resource that is already installed" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -WarningVariable WarningVar -warningaction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + } + + It "Reinstall resource that is already installed with -Reinstall parameter" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -Reinstall -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Restore resource after reinstall fails" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-Module $testModuleName -ListAvailable + $pkg.Name | Should -Contain $testModuleName + $pkg.Version | Should -Contain "5.0.0.0" + + $resourcePath = Split-Path -Path $pkg.Path -Parent + $resourceFiles = Get-ChildItem -Path $resourcePath -Recurse + + # Lock resource file to prevent reinstall from succeeding. + $fs = [System.IO.File]::Open($resourceFiles[0].FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + try + { + # Reinstall of resource should fail with one of its files locked. + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Reinstall -ErrorVariable ev -ErrorAction Silent + $ev.FullyQualifiedErrorId | Should -BeExactly 'InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource' + } + finally + { + $fs.Close() + } + + # Verify that resource module has been restored. + (Get-ChildItem -Path $resourcePath -Recurse).Count | Should -BeExactly $resourceFiles.Count + } + + # It "Install resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName -AcceptLicense + # $pkg = Get-PSResource "testModuleWithlicense" + # $pkg.Name | Should -Be "testModuleWithlicense" + # $pkg.Version | Should -Be "0.0.3.0" + # } + + + It "Install resource with cmdlet names from a module already installed (should clobber)" { + Install-PSResource -Name "CLobberTestModule1" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource "ClobberTestModule1" + $pkg.Name | Should -Be "ClobberTestModule1" + $pkg.Version | Should -Be "0.0.1.0" + + Install-PSResource -Name "ClobberTestModule2" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource "ClobberTestModule2" + $pkg.Name | Should -Be "ClobberTestModule2" + $pkg.Version | Should -Be "0.0.1.0" + } + + It "Install resource from local repository given Repository parameter" { + $publishModuleName = "TestFindModule" + $repoName = "psgettestlocal" + Get-ModuleResourcePublishedToLocalRepoTestDrive $publishModuleName $repoName + Set-PSResourceRepository "psgettestlocal" -Trusted:$true + + Install-PSResource -Name $publishModuleName -Repository $repoName + $pkg = Get-PSResource $publishModuleName + $pkg | Should -Not -BeNullOrEmpty + $pkg.Name | Should -Be $publishModuleName + } + + It "Install module using -WhatIf, should not install the module" { + Install-PSResource -Name $testModuleName -WhatIf + + $res = Get-PSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Validates that a module with module-name script files (like Pester) installs under Modules path" { + + Install-PSResource -Name "testModuleWithScript" -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource "testModuleWithScript" + $res.InstalledLocation.ToString().Contains("Modules") | Should -Be $true + } + + It "Install module using -NoClobber, should throw clobber error and not install the module" { + Install-PSResource -Name "ClobberTestModule1" -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource "ClobberTestModule1" + $res.Name | Should -Be "ClobberTestModule1" + + Install-PSResource -Name "ClobberTestModule2" -Repository $PSGalleryName -TrustRepository -NoClobber -ErrorAction SilentlyContinue + $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + It "Install PSResourceInfo object piped in" { + Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName | Install-PSResource -TrustRepository + $res = Get-PSResource -Name $testModuleName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "1.0.0.0" + } + + It "Install module using -PassThru" { + $res = Install-PSResource -Name $testModuleName -Repository $PSGalleryName -PassThru -TrustRepository + $res.Name | Should -Contain $testModuleName + } + + It "Install modules using -RequiredResource with hashtable" { + $rrHash = @{ + test_module = @{ + version = "[1.0.0,5.0.0)" + repository = $PSGalleryName + } + + test_module2 = @{ + version = "[1.0.0,3.0.0)" + repository = $PSGalleryName + prerelease = "true" + } + + TestModule99 = @{} + } + + Install-PSResource -RequiredResource $rrHash -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93.0" + } + + It "Install modules using -RequiredResource with JSON string" { + $rrJSON = "{ + 'test_module': { + 'version': '[1.0.0,5.0.0)', + 'repository': 'PSGallery' + }, + 'test_module2': { + 'version': '[1.0.0,3.0.0)', + 'repository': 'PSGallery', + 'prerelease': 'true' + }, + 'TestModule99': { + 'repository': 'PSGallery' + } + }" + + Install-PSResource -RequiredResource $rrJSON -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93.0" + } + + It "Install modules using -RequiredResourceFile with PSD1 file" { + $rrFilePSD1 = Join-Path -Path $psscriptroot -ChildPath $RequiredResourcePSD1FileName + + Install-PSResource -RequiredResourceFile $rrFilePSD1 -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93.0" + } + + It "Install modules using -RequiredResourceFile with JSON file" { + $rrFileJSON = Join-Path -Path $psscriptroot -ChildPath $RequiredResourceJSONFileName + + Install-PSResource -RequiredResourceFile $rrFileJSON -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93.0" + } + + # Install module 1.4.3 (is authenticode signed and has catalog file) + # Should install successfully + It "Install modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.3" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.3.0" + } + + # Install module 1.4.7 (is authenticode signed and has no catalog file) + # Should not install successfully + It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.7" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.7.0" + } + + # Install module that is not authenticode signed + # Should FAIL to install the module + It "Install module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { + { Install-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # Install 1.4.4.1 (with incorrect catalog file) + # Should FAIL to install the module + It "Install module with incorrect catalog file" -Skip:(!(Get-IsWindows)) { + { Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "TestFileCatalogError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # Install script that is signed + # Should install successfully + It "Install script that is authenticode signed" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource "Install-VSCode" -Version "1.4.2" + $res1.Name | Should -Be "Install-VSCode" + $res1.Version | Should -Be "1.4.2.0" + } + + # Install script that is not signed + # Should throw + It "Install script that is not signed" -Skip:(!(Get-IsWindows)) { + { Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } +} + +<# Temporarily commented until -Tag is implemented for this Describe block +Describe 'Test Install-PSResource for interactive and root user scenarios' { + + BeforeAll{ + $TestGalleryName = Get-PoshTestGalleryName + $PSGalleryName = Get-PSGalleryName + $NuGetGalleryName = Get-NuGetGalleryName + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource "TestModule", "testModuleWithlicense" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + # Unix only manual test + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + It "Install resource under AllUsers scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name "TestModule" -Repository $TestGalleryName -Scope AllUsers + $pkg = Get-Module "TestModule" -ListAvailable + $pkg.Name | Should -Be "TestModule" + $pkg.Path.Contains("/usr/") | Should -Be $true + } + + # This needs to be manually tested due to prompt + It "Install resource that requires accept license without -AcceptLicense flag" { + Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName + $pkg = Get-PSResource "testModuleWithlicense" + $pkg.Name | Should -Be "testModuleWithlicense" + $pkg.Version | Should -Be "0.0.1.0" + } + + # This needs to be manually tested due to prompt + It "Install resource should prompt 'trust repository' if repository is not trusted" { + Set-PSResourceRepository PoshTestGallery -Trusted:$false + + Install-PSResource -Name "TestModule" -Repository $TestGalleryName -confirm:$false + + $pkg = Get-Module "TestModule" -ListAvailable + $pkg.Name | Should -Be "TestModule" + + Set-PSResourceRepository PoshTestGallery -Trusted + } +} +#> diff --git a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 new file mode 100644 index 000000000..cda98947c --- /dev/null +++ b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 @@ -0,0 +1,275 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test Install-PSResource for local repositories' { + + + BeforeAll { + $localRepo = "psgettestlocal" + $moduleName = "test_local_mod" + $moduleName2 = "test_local_mod2" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "1.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "3.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "5.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName2 $localRepo "1.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName2 $localRepo "5.0.0" + } + + AfterEach { + Uninstall-PSResource $moduleName, $moduleName2 -Version "*" + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Update resource installed given Name parameter" { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + It "Update resources installed given Name (with wildcard) parameter" { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository + Install-PSResource -Name $moduleName2 -Version "1.0.0" -Repository $localRepo -TrustRepository + + Update-PSResource -Name "test_local*" -Repository $localRepo -TrustRepository + $res = Get-PSResource -Name "test_local*" -Version "5.0.0" + + $inputHashtable = @{test_module = "1.0.0"; test_module2 = "1.0.0"} + $isTest_ModuleUpdated = $false + $isTest_Module2Updated = $false + foreach ($item in $res) + { + if ([System.Version]$item.Version -gt [System.Version]$inputHashtable[$item.Name]) + { + if ($item.Name -like $moduleName) + { + $isTest_ModuleUpdated = $true + } + elseif ($item.Name -like $moduleName2) + { + $isTest_Module2Updated = $true + } + } + } + + $isTest_ModuleUpdated | Should -BeTrue + $isTest_Module2Updated | Should -BeTrue + } + + It "Update resource installed given Name and Version (specific) parameters" { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository + + Update-PSResource -Name $moduleName -Version "5.0.0" -Repository $localRepo -TrustRepository + $res = Get-PSResource -Name $moduleName + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -eq [System.Version]"5.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -BeTrue + } + + # Windows only + It "update resource under CurrentUser scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + # TODO: perhaps also install TestModule with the highest version (the one above 1.2.0.0) to the AllUsers path too + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope AllUsers + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $moduleName -Version "3.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Windows only + It "update resource under AllUsers scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository -Scope AllUsers + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository -Scope AllUsers + + $res = Get-Module -Name $moduleName -ListAvailable + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Contain "5.0.0" + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.ModuleBase.Contains("Program") | Should -Be $true + $isPkgUpdated = $true + } + } + $isPkgUpdated | Should -Be $true + + } + + # Windows only + It "Update resource under no specified scope" -skip:(!$IsWindows) { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository + Update-PSResource -Name $moduleName -Version "5.0.0.0" -Repository $localRepo -TrustRepository + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + # this line is commented out because AllUsers scope requires sudo and that isn't supported in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + # this test is skipped because it requires sudo to run and has yet to be resolved in CI + It "Update resource under AllUsers scope - Unix only" -Skip:($true) { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope AllUsers + + Update-PSResource -Name $moduleName -Repository $PSGalleryName -TrustRepository -Scope AllUsers + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("usr") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + # this is commented out because it requires sudo to run with AllUsers scope and this hasn't been resolved in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # It "update resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "TestModuleWithLicense" -Version "0.0.1.0" -Repository $TestGalleryName -AcceptLicense + # Update-PSResource -Name "TestModuleWithLicense" -Repository $TestGalleryName -AcceptLicense + # $res = Get-PSResource "TestModuleWithLicense" + + # $isPkgUpdated = $false + # foreach ($pkg in $res) + # { + # if ([System.Version]$pkg.Version -gt [System.Version]"0.0.1.0") + # { + # $isPkgUpdated = $true + # } + # } + + # $isPkgUpdated | Should -Be $true + # } + + It "Update module using -WhatIf, should not update the module" { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository + Update-PSResource -Name $moduleName -WhatIf -Repository $localRepo -TrustRepository + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $false + } + + It "Update resource installed given -Name and -PassThru parameters" { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository + + $res = Update-PSResource -Name $moduleName -Version "5.0.0.0" -Repository $localRepo -TrustRepository -PassThru + $res.Name | Should -Contain $moduleName + $res.Version | Should -Be "5.0.0.0" + } +} diff --git a/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 new file mode 100644 index 000000000..9a83695eb --- /dev/null +++ b/test/InstallPSResourceTests/InstallPSResourceV2Server.Tests.ps1 @@ -0,0 +1,543 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test Install-PSResource for V2 Server scenarios' { + + BeforeAll { + $PSGalleryName = Get-PSGalleryName + $PSGalleryUri = Get-PSGalleryLocation + $NuGetGalleryName = Get-NuGetGalleryName + $testModuleName = "test_module" + $testModuleName2 = "TestModule99" + $testScriptName = "test_script" + $PackageManagement = "PackageManagement" + $RequiredResourceJSONFileName = "TestRequiredResourceFile.json" + $RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule","ClobberTestModule1", "ClobberTestModule2", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, + @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, + @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} + + It "Should not install resource with wildcard in name" -TestCases $testCases { + param($Name, $ErrorId) + Install-PSResource -Name $Name -Repository $PSGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + It "Install specific module resource by name" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Install specific script resource by name" { + Install-PSResource -Name $testScriptName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testScriptName + $pkg.Name | Should -Be $testScriptName + $pkg.Version | Should -Be "3.5.0.0" + } + + It "Install multiple resources by name" { + $pkgNames = @($testModuleName, $testModuleName2) + Install-PSResource -Name $pkgNames -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $pkgNames + $pkg.Name | Should -Be $pkgNames + } + + It "Should not install resource given nonexistant name" { + Install-PSResource -Name "NonExistantModule" -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $pkg = Get-PSResource "NonExistantModule" + $pkg.Name | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # Do some version testing, but Find-PSResource should be doing thorough testing + It "Should install resource given name and exact version" { + Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "1.0.0.0" + } + + It "Should install resource given name and exact version with bracket syntax" { + Install-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "1.0.0.0" + } + + It "Should install resource given name and exact range inclusive [1.0.0, 5.0.0]" { + Install-PSResource -Name $testModuleName -Version "[1.0.0, 5.0.0]" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Should install resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Install-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "3.0.0.0" + } + + # TODO: Update this test and others like it that use try/catch blocks instead of Should -Throw + It "Should not install resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version = "(1.0.0.0)" + try { + Install-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -TrustRepository -ErrorAction SilentlyContinue + } + catch + {} + $Error[0].FullyQualifiedErrorId | Should -be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + + $res = Get-PSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Install resource when given Name, Version '*', should install the latest version" { + Install-PSResource -Name $testModuleName -Version "*" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Install resource with latest (including prerelease) version given Prerelease parameter" { + Install-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + } + + It "Install a module with a dependency" { + Uninstall-PSResource -Name "TestModuleWithDependency*" -Version "*" -SkipDependencyCheck + Install-PSResource -Name "TestModuleWithDependencyC" -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository + + $pkg = Get-PSResource "TestModuleWithDependencyC" + $pkg.Name | Should -Be "TestModuleWithDependencyC" + $pkg.Version | Should -Be "1.0" + + $pkg = Get-PSResource "TestModuleWithDependencyB" + $pkg.Name | Should -Be "TestModuleWithDependencyB" + $pkg.Version | Should -Be "3.0" + + $pkg = Get-PSResource "TestModuleWithDependencyD" + $pkg.Name | Should -Be "TestModuleWithDependencyD" + $pkg.Version | Should -Be "1.0" + } + + It "Install a module with a dependency and skip installing the dependency" { + Uninstall-PSResource -Name "TestModuleWithDependency*" -Version "*" -SkipDependencyCheck + Install-PSResource -Name "TestModuleWithDependencyC" -Version "1.0.0.0" -SkipDependencyCheck -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource "TestModuleWithDependencyC" + $pkg.Name | Should -Be "TestModuleWithDependencyC" + $pkg.Version | Should -Be "1.0" + + $pkg = Get-PSResource "TestModuleWithDependencyB", "TestModuleWithDependencyD" + $pkg | Should -BeNullOrEmpty + } + + It "Install resource via InputObject by piping from Find-PSresource" { + Find-PSResource -Name $testModuleName -Repository $PSGalleryName | Install-PSResource -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + It "Install resource under specified in PSModulePath" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + ($env:PSModulePath).Contains($pkg.InstalledLocation) + } + + It "Install resource with companyname, copyright and repository source location and validate" { + Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository PSGallery -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + + $pkg.CompanyName | Should -Be "Anam" + $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." + $pkg.RepositorySourceLocation | Should -Be $PSGalleryUri + } + + + It "Install script with companyname, copyright, and repository source location and validate" { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource "Install-VSCode" -Version "1.4.2" + $res.Name | Should -Be "Install-VSCode" + $res.Version | Should -Be "1.4.2" + $res.CompanyName | Should -Be "Microsoft Corporation" + $res.Copyright | Should -Be "(c) Microsoft Corporation" + $res.RepositorySourceLocation | Should -Be $PSGalleryUri + } + + # Windows only + It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + } + + # Windows only + It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { + Install-PSResource -Name "testmodule99" -Repository $PSGalleryName -TrustRepository -Scope AllUsers -Verbose + $pkg = Get-Module "testmodule99" -ListAvailable + $pkg.Name | Should -Be "testmodule99" + $pkg.Path.ToString().Contains("Program Files") + } + + # Windows only + It "Install resource under no specified scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Install resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Install resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true + } + + It "Should not install resource that is already installed" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -WarningVariable WarningVar -warningaction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + } + + It "Reinstall resource that is already installed with -Reinstall parameter" { + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + Install-PSResource -Name $testModuleName -Repository $PSGalleryName -Reinstall -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0.0" + } + + # It "Restore resource after reinstall fails" { + # Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + # $pkg = Get-PSResource $testModuleName + # $pkg.Name | Should -Contain $testModuleName + # $pkg.Version | Should -Contain "5.0.0.0" + + # $resourcePath = Split-Path -Path $pkg.InstalledLocation -Parent + # $resourceFiles = Get-ChildItem -Path $resourcePath -Recurse + + # # Lock resource file to prevent reinstall from succeeding. + # $fs = [System.IO.File]::Open($resourceFiles[0].FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + # try + # { + # # Reinstall of resource should fail with one of its files locked. + # Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Reinstall -ErrorVariable ev -ErrorAction Silent + # $ev.FullyQualifiedErrorId | Should -BeExactly 'InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource' + # } + # finally + # { + # $fs.Close() + # } + + # # Verify that resource module has been restored. + # (Get-ChildItem -Path $resourcePath -Recurse).Count | Should -BeExactly $resourceFiles.Count + # } + + # It "Install resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName -AcceptLicense + # $pkg = Get-PSResource "testModuleWithlicense" + # $pkg.Name | Should -Be "testModuleWithlicense" + # $pkg.Version | Should -Be "0.0.3.0" + # } + + + It "Install resource with cmdlet names from a module already installed (should clobber)" { + Install-PSResource -Name "CLobberTestModule1" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource "ClobberTestModule1" + $pkg.Name | Should -Be "ClobberTestModule1" + $pkg.Version | Should -Be "0.0.1" + + Install-PSResource -Name "ClobberTestModule2" -Repository $PSGalleryName -TrustRepository + $pkg = Get-PSResource "ClobberTestModule2" + $pkg.Name | Should -Be "ClobberTestModule2" + $pkg.Version | Should -Be "0.0.1" + } + + It "Install module using -WhatIf, should not install the module" { + Install-PSResource -Name $testModuleName -WhatIf + + $res = Get-PSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Validates that a module with module-name script files (like Pester) installs under Modules path" { + + Install-PSResource -Name "testModuleWithScript" -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource "testModuleWithScript" + $res.InstalledLocation.ToString().Contains("Modules") | Should -Be $true + } + + # It "Install module using -NoClobber, should throw clobber error and not install the module" { + # Install-PSResource -Name "ClobberTestModule1" -Repository $PSGalleryName -TrustRepository + + # $res = Get-PSResource "ClobberTestModule1" + # $res.Name | Should -Be "ClobberTestModule1" + + # Install-PSResource -Name "ClobberTestModule2" -Repository $PSGalleryName -TrustRepository -NoClobber -ErrorAction SilentlyContinue + # $Error[0].FullyQualifiedErrorId | Should -be "CommandAlreadyExists,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + # } + + It "Install PSResourceInfo object piped in" { + Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName | Install-PSResource -TrustRepository + $res = Get-PSResource -Name $testModuleName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "1.0.0.0" + } + + It "Install module using -PassThru" { + $res = Install-PSResource -Name $testModuleName -Repository $PSGalleryName -PassThru -TrustRepository + $res.Name | Should -Contain $testModuleName + } + + It "Install modules using -RequiredResource with hashtable" { + $rrHash = @{ + test_module = @{ + version = "[1.0.0,5.0.0)" + repository = $PSGalleryName + } + + test_module2 = @{ + version = "[1.0.0,3.0.0)" + repository = $PSGalleryName + prerelease = "true" + } + + TestModule99 = @{ + repository = $PSGalleryName + } + } + + Install-PSResource -RequiredResource $rrHash -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93" + } + + It "Install modules using -RequiredResource with JSON string" { + $rrJSON = "{ + 'test_module': { + 'version': '[1.0.0,5.0.0)', + 'repository': 'PSGallery' + }, + 'test_module2': { + 'version': '[1.0.0,3.0.0)', + 'repository': 'PSGallery', + 'prerelease': 'true' + }, + 'TestModule99': { + 'repository': 'PSGallery' + } + }" + + Install-PSResource -RequiredResource $rrJSON -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93" + } + + It "Install modules using -RequiredResourceFile with PSD1 file" { + $rrFilePSD1 = Join-Path -Path "$((Get-Item $psscriptroot).parent)" -ChildPath $RequiredResourcePSD1FileName + + Install-PSResource -RequiredResourceFile $rrFilePSD1 -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93" + } + + It "Install modules using -RequiredResourceFile with JSON file" { + $rrFileJSON = Join-Path -Path "$((Get-Item $psscriptroot).parent)" -ChildPath $RequiredResourceJSONFileName + + Install-PSResource -RequiredResourceFile $rrFileJSON -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource "test_module2" -Version "2.5.0-beta" + $res2.Name | Should -Be "test_module2" + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource $testModuleName2 + $res3.Name | Should -Be $testModuleName2 + $res3.Version | Should -Be "0.0.93" + } + + # Install module 1.4.3 (is authenticode signed and has catalog file) + # Should install successfully + It "Install modules with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.3" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.3" + } + + # Install module 1.4.7 (is authenticode signed and has no catalog file) + # Should not install successfully + It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $PackageManagement -Version "1.4.7" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource $PackageManagement -Version "1.4.7" + $res1.Name | Should -Be $PackageManagement + $res1.Version | Should -Be "1.4.7" + } + + # Install module that is not authenticode signed + # Should FAIL to install the module + It "Install module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # # Install 1.4.4.1 (with incorrect catalog file) + # # Should FAIL to install the module + # It "Install module with incorrect catalog file" -Skip:(!(Get-IsWindows)) { + # { Install-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "TestFileCatalogError,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + # } + + # Install script that is signed + # Should install successfully + It "Install script that is authenticode signed" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + + $res1 = Get-PSResource "Install-VSCode" -Version "1.4.2" + $res1.Name | Should -Be "Install-VSCode" + $res1.Version | Should -Be "1.4.2" + } + + # Install script that is not signed + # Should throw + It "Install script that is not signed" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } +} + +<# Temporarily commented until -Tag is implemented for this Describe block +Describe 'Test Install-PSResource for interactive and root user scenarios' { + + BeforeAll{ + $TestGalleryName = Get-PoshTestGalleryName + $PSGalleryName = Get-PSGalleryName + $NuGetGalleryName = Get-NuGetGalleryName + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource "TestModule", "testModuleWithlicense" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + # Unix only manual test + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + It "Install resource under AllUsers scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name "TestModule" -Repository $TestGalleryName -Scope AllUsers + $pkg = Get-Module "TestModule" -ListAvailable + $pkg.Name | Should -Be "TestModule" + $pkg.Path.Contains("/usr/") | Should -Be $true + } + + # This needs to be manually tested due to prompt + It "Install resource that requires accept license without -AcceptLicense flag" { + Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName + $pkg = Get-PSResource "testModuleWithlicense" + $pkg.Name | Should -Be "testModuleWithlicense" + $pkg.Version | Should -Be "0.0.1.0" + } + + # This needs to be manually tested due to prompt + It "Install resource should prompt 'trust repository' if repository is not trusted" { + Set-PSResourceRepository PoshTestGallery -Trusted:$false + + Install-PSResource -Name "TestModule" -Repository $TestGalleryName -confirm:$false + + $pkg = Get-Module "TestModule" -ListAvailable + $pkg.Name | Should -Be "TestModule" + + Set-PSResourceRepository PoshTestGallery -Trusted + } +} +#> diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 new file mode 100644 index 000000000..2bd9b3b8d --- /dev/null +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -0,0 +1,410 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test Install-PSResource for V3Server scenarios' { + + BeforeAll { + $NuGetGalleryName = Get-NuGetGalleryName + $NuGetGalleryUri = Get-NuGetGalleryLocation + $testModuleName = "test_module" + $testModuleName2 = "test_module2" + # $testModuleName2 = "TestModule99" + $testScriptName = "test_script" + $PackageManagement = "PackageManagement" + $RequiredResourceJSONFileName = "TestRequiredResourceFile.json" + $RequiredResourcePSD1FileName = "TestRequiredResourceFile.psd1" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, + @{Name="Test_Module*"; ErrorId="NameContainsWildcard"}, + @{Name="Test?Module","Test[Module"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} + + It "Should not install resource with wildcard in name" -TestCases $testCases { + param($Name, $ErrorId) + Install-PSResource -Name $Name -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + It "Install specific module resource by name" { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + It "Install specific script resource by name" { + Install-PSResource -Name $testScriptName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testScriptName + $pkg.Name | Should -Be $testScriptName + $pkg.Version | Should -Be "3.5.0" + } + + It "Install multiple resources by name" { + $pkgNames = @($testModuleName, $testModuleName2) + Install-PSResource -Name $pkgNames -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $pkgNames + $pkg.Name | Should -Be $pkgNames + } + + It "Should not install resource given nonexistant name" { + Install-PSResource -Name "NonExistantModule" -Repository $NuGetGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $pkg = Get-PSResource "NonExistantModule" + $pkg.Name | Should -BeNullOrEmpty + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + } + + # Do some version testing, but Find-PSResource should be doing thorough testing + It "Should install resource given name and exact version" { + Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "1.0.0" + } + + It "Should install resource given name and exact version with bracket syntax" { + Install-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "1.0.0" + } + + It "Should install resource given name and exact range inclusive [1.0.0, 5.0.0]" { + Install-PSResource -Name $testModuleName -Version "[1.0.0, 5.0.0]" -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + It "Should install resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Install-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "3.0.0" + } + + # TODO: Update this test and others like it that use try/catch blocks instead of Should -Throw + It "Should not install resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version = "(1.0.0.0)" + try { + Install-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -TrustRepository -ErrorAction SilentlyContinue + } + catch + {} + $Error[0].FullyQualifiedErrorId | Should -be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource" + + $res = Get-PSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Install resource when given Name, Version '*', should install the latest version" { + Install-PSResource -Name $testModuleName -Version "*" -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + It "Install resource with latest (including prerelease) version given Prerelease parameter" { + Install-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + } + + It "Install resource via InputObject by piping from Find-PSresource" { + Find-PSResource -Name $testModuleName -Repository $NuGetGalleryName | Install-PSResource -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + It "Install resource under specified in PSModulePath" { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + ($env:PSModulePath).Contains($pkg.InstalledLocation) + } + + It "Install resource with companyname, copyright and repository source location and validate properties" { + Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + + $pkg.CompanyName | Should -Be "Anam Navied" + $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." + $pkg.RepositorySourceLocation | Should -Be $NuGetGalleryUri + } + + # Windows only + It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + } + + # Windows only + It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { + Install-PSResource -Name "testmodule99" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers -Verbose + $pkg = Get-Module "testmodule99" -ListAvailable + $pkg.Name | Should -Be "testmodule99" + $pkg.Path.ToString().Contains("Program Files") + } + + # Windows only + It "Install resource under no specified scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Documents") | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Install resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Install resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("$env:HOME/.local") | Should -Be $true + } + + It "Should not install resource that is already installed" { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -WarningVariable WarningVar -warningaction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + } + + It "Reinstall resource that is already installed with -Reinstall parameter" { + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -Reinstall -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + # It "Restore resource after reinstall fails" { + # Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + # $pkg = Get-PSResource $testModuleName + # $pkg.Name | Should -Contain $testModuleName + # $pkg.Version | Should -Contain "5.0.0" + + # $resourcePath = Split-Path -Path $pkg.InstalledLocation -Parent + # $resourceFiles = Get-ChildItem -Path $resourcePath -Recurse + + # # Lock resource file to prevent reinstall from succeeding. + # $fs = [System.IO.File]::Open($resourceFiles[0].FullName, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read) + # try + # { + # # Reinstall of resource should fail with one of its files locked. + # Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository -Reinstall -ErrorVariable ev -ErrorAction Silent + # $ev.FullyQualifiedErrorId | Should -BeExactly 'InstallPackageFailed,Microsoft.PowerShell.PowerShellGet.Cmdlets.InstallPSResource' + # } + # finally + # { + # $fs.Close() + # } + + # # Verify that resource module has been restored. + # (Get-ChildItem -Path $resourcePath -Recurse).Count | Should -BeExactly $resourceFiles.Count + # } + + # It "Install resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName -AcceptLicense + # $pkg = Get-PSResource "testModuleWithlicense" + # $pkg.Name | Should -Be "testModuleWithlicense" + # $pkg.Version | Should -Be "0.0.3.0" + # } + + It "Install PSResourceInfo object piped in" { + Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName | Install-PSResource -TrustRepository + $res = Get-PSResource -Name $testModuleName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "1.0.0" + } + + It "Install module using -PassThru" { + $res = Install-PSResource -Name $testModuleName -Repository $NuGetGalleryName -PassThru -TrustRepository + $res.Name | Should -Contain $testModuleName + } + + It "Install modules using -RequiredResource with hashtable" { + $rrHash = @{ + test_module = @{ + version = "[1.0.0,5.0.0)" + repository = $NuGetGalleryName + } + + test_module2 = @{ + version = "[1.0.0,5.0.0]" + repository = $NuGetGalleryName + prerelease = "true" + } + + TestModule99 = @{ + repository = $NuGetGalleryName + } + } + + Install-PSResource -RequiredResource $rrHash -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0" + + $res2 = Get-PSResource $testModuleName2 + $res2.Name | Should -Be $testModuleName2 + $res2.Version | Should -Be "5.0.0" + + $res3 = Get-PSResource "TestModule99" + $res3.Name | Should -Be "TestModule99" + $res3.Version | Should -Be "0.0.93" + } + + It "Install modules using -RequiredResource with JSON string" { + $rrJSON = "{ + 'test_module': { + 'version': '[1.0.0,5.0.0)', + 'repository': 'NuGetGallery' + }, + 'test_module2': { + 'version': '[1.0.0,5.0.0]', + 'repository': 'PSGallery', + 'prerelease': 'true' + }, + 'TestModule99': { + 'repository': 'NuGetGallery' + } + }" + + Install-PSResource -RequiredResource $rrJSON -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0" + + $res2 = Get-PSResource $testModuleName2 + $res2.Name | Should -Be $testModuleName2 + $res2.Version | Should -Be "5.0.0.0" + + $res3 = Get-PSResource "testModule99" + $res3.Name | Should -Be "testModule99" + $res3.Version | Should -Be "0.0.93" + } + + It "Install modules using -RequiredResourceFile with PSD1 file" { + $rrFilePSD1 = Join-Path -Path "$((Get-Item $psscriptroot).parent)" -ChildPath $RequiredResourcePSD1FileName + + Install-PSResource -RequiredResourceFile $rrFilePSD1 -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource $testModuleName2 -Version "2.5.0-beta" + $res2.Name | Should -Be $testModuleName2 + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource "testModule99" + $res3.Name | Should -Be "testModule99" + $res3.Version | Should -Be "0.0.93" + } + + It "Install modules using -RequiredResourceFile with JSON file" { + $rrFileJSON = Join-Path -Path "$((Get-Item $psscriptroot).parent)" -ChildPath $RequiredResourceJSONFileName + + Install-PSResource -RequiredResourceFile $rrFileJSON -TrustRepository + + $res1 = Get-PSResource $testModuleName + $res1.Name | Should -Be $testModuleName + $res1.Version | Should -Be "3.0.0.0" + + $res2 = Get-PSResource $testModuleName2 -Version "2.5.0-beta" + $res2.Name | Should -Be $testModuleName2 + $res2.Version | Should -Be "2.5.0" + $res2.Prerelease | Should -Be "beta" + + $res3 = Get-PSResource "testModule99" + $res3.Name | Should -Be "testModule99" + $res3.Version | Should -Be "0.0.93" + } +} +<# Temporarily commented until -Tag is implemented for this Describe block +Describe 'Test Install-PSResource for interactive and root user scenarios' { + + BeforeAll{ + $TestGalleryName = Get-PoshTestGalleryName + $NuGetGalleryName = Get-PSGalleryName + $NuGetGalleryName = Get-NuGetGalleryName + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource "TestModule", "testModuleWithlicense" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + # Unix only manual test + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + It "Install resource under AllUsers scope - Unix only" -Skip:(Get-IsWindows) { + Install-PSResource -Name "TestModule" -Repository $TestGalleryName -Scope AllUsers + $pkg = Get-Module "TestModule" -ListAvailable + $pkg.Name | Should -Be "TestModule" + $pkg.Path.Contains("/usr/") | Should -Be $true + } + + # This needs to be manually tested due to prompt + It "Install resource that requires accept license without -AcceptLicense flag" { + Install-PSResource -Name "testModuleWithlicense" -Repository $TestGalleryName + $pkg = Get-PSResource "testModuleWithlicense" + $pkg.Name | Should -Be "testModuleWithlicense" + $pkg.Version | Should -Be "0.0.1.0" + } + + # This needs to be manually tested due to prompt + It "Install resource should prompt 'trust repository' if repository is not trusted" { + Set-PSResourceRepository PoshTestGallery -Trusted:$false + + Install-PSResource -Name "TestModule" -Repository $TestGalleryName -confirm:$false + + $pkg = Get-Module "TestModule" -ListAvailable + $pkg.Name | Should -Be "TestModule" + + Set-PSResourceRepository PoshTestGallery -Trusted + } +} +#> diff --git a/test/PSGetTestUtils.psm1 b/test/PSGetTestUtils.psm1 index e72984f60..759aa433a 100644 --- a/test/PSGetTestUtils.psm1 +++ b/test/PSGetTestUtils.psm1 @@ -19,10 +19,8 @@ $script:IsCoreCLR = $PSVersionTable.ContainsKey('PSEdition') -and $PSVersionTabl $script:PSGalleryName = 'PSGallery' $script:PSGalleryLocation = 'https://www.powershellgallery.com/api/v2' -$script:PoshTestGalleryName = 'PoshTestGallery' -$script:PostTestGalleryLocation = 'https://www.poshtestgallery.com/api/v2' - $script:NuGetGalleryName = 'NuGetGallery' +$script:NuGetGalleryLocation = 'https://api.nuget.org/v3/index.json' if($script:IsInbox) { @@ -141,6 +139,12 @@ function Get-NuGetGalleryName { return $script:NuGetGalleryName } + +function Get-NuGetGalleryLocation +{ + return $script:NuGetGalleryLocation +} + function Get-PSGalleryName { return $script:PSGalleryName @@ -149,15 +153,6 @@ function Get-PSGalleryName function Get-PSGalleryLocation { return $script:PSGalleryLocation } - -function Get-PoshTestGalleryName { - return $script:PoshTestGalleryName -} - -function Get-PoshTestGalleryLocation { - return $script:PostTestGalleryLocation -} - function Get-NewTestDirs { Param( [string[]] @@ -256,6 +251,18 @@ function Register-LocalRepos { Write-Verbose("registered psgettestlocal, psgettestlocal2") } +function Register-PSGallery { + $PSGalleryRepoParams = @{ + Name = $script:PSGalleryName + Uri = $script:PSGalleryLocation + Priority = 1 + Trusted = $false + } + Register-PSResourceRepository @PSGalleryRepoParams + + Write-Verbose("registered PSGallery") +} + function Unregister-LocalRepos { if(Get-PSResourceRepository -Name "psgettestlocal"){ Unregister-PSResourceRepository -Name "psgettestlocal" @@ -386,6 +393,57 @@ function Get-ModuleResourcePublishedToLocalRepoTestDrive Publish-PSResource -Path $publishModuleBase -Repository $repoName } +function Get-ModuleResourcePublishedToLocalRepoTestDrive +{ + Param( + [string] + $moduleName, + + [string] + $repoName, + + [string] + $packageVersion, + + [string] + $prereleaseLabel, + + [string[]] + $tags + ) + Get-TestDriveSetUp + + $publishModuleName = $moduleName + $publishModuleBase = Join-Path $script:testIndividualResourceFolder $publishModuleName + $null = New-Item -Path $publishModuleBase -ItemType Directory -Force + + $version = $packageVersion + if (!$tags -or ($tags.Count -eq 0)) + { + if (!$prereleaseLabel) + { + New-ModuleManifest -Path (Join-Path -Path $publishModuleBase -ChildPath "$publishModuleName.psd1") -ModuleVersion $version -Description "$publishModuleName module" + } + else + { + New-ModuleManifest -Path (Join-Path -Path $publishModuleBase -ChildPath "$publishModuleName.psd1") -ModuleVersion $version -Prerelease $prereleaseLabel -Description "$publishModuleName module" + } + } + else { + # tags is not null or is empty + if (!$prereleaseLabel) + { + New-ModuleManifest -Path (Join-Path -Path $publishModuleBase -ChildPath "$publishModuleName.psd1") -ModuleVersion $version -Description "$publishModuleName module" -Tags $tags + } + else + { + New-ModuleManifest -Path (Join-Path -Path $publishModuleBase -ChildPath "$publishModuleName.psd1") -ModuleVersion $version -Prerelease $prereleaseLabel -Description "$publishModuleName module" -Tags $tags + } + } + + Publish-PSResource -Path $publishModuleBase -Repository $repoName +} + function Register-LocalRepos { $repoUriAddress = Join-Path -Path $TestDrive -ChildPath "testdir" $null = New-Item $repoUriAddress -ItemType Directory -Force diff --git a/test/SavePSResourceTests/SavePSResourceAATests.ps1 b/test/SavePSResourceTests/SavePSResourceAATests.ps1 new file mode 100644 index 000000000..7e9e9f3d7 --- /dev/null +++ b/test/SavePSResourceTests/SavePSResourceAATests.ps1 @@ -0,0 +1,163 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# WIP +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Save-PSResource for Azure Artifacts' { + + BeforeAll { + $AzureArtifactsName = "AzureArtifacts" + $testModuleName = "test_module" + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $AzureArtifactsName -Uri "https://pkgs.dev.azure.com/PowerShellGetTesting/PSGetTesting/_packaging/PSGetTest/nuget/v3/index.json" + + $SaveDir = Join-Path $TestDrive 'SavedResources' + New-Item -Item Directory $SaveDir -Force + } + + AfterEach { + # Delete contents of save directory + Remove-Item -Path (Join-Path $SaveDir '*') -Recurse -Force -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Save specific module resource by name" { + Save-PSResource -Name $testModuleName -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem $pkgDir.FullName).Count | Should -Be 1 + } + +### TODO: Fix this test + It "Save multiple resources by name" { + $pkgNames = @($testModuleName, $testModuleName2) + Save-PSResource -Name $pkgNames -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq $testModuleName -or $_.Name -eq $testModuleName2 } + $pkgDirs.Count | Should -Be 2 + (Get-ChildItem $pkgDirs[0].FullName).Count | Should -Be 1 + (Get-ChildItem $pkgDirs[1].FullName).Count | Should -Be 1 + } + + It "Should not save resource given nonexistant name" { + Save-PSResource -Name NonExistentModule -Repository $AzureArtifactsRepo -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "NonExistentModule" + $pkgDir.Name | Should -BeNullOrEmpty + } + + It "Not Save module with Name containing wildcard" { + Save-PSResource -Name "TestModule*" -Repository $AzureArtifactsName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "NameContainsWildcard,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Should save resource given name and exact version" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + } + + It "Should save resource given name and exact version with bracket syntax" { + Save-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + } + + It "Should save resource given name and exact range inclusive [1.0.0, 3.0.0]" { + Save-PSResource -Name $testModuleName -Version "[1.0.0, 3.0.0]" -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0" + } + + It "Should save resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Save-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0" + } + + It "Should not save resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version="(1.0.0.0)" + try { + Save-PSResource -Name $testModuleName -Version $Version -Repository $AzureArtifactsName -Path $SaveDir -ErrorAction SilentlyContinue -TrustRepository + } + catch + {} + + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -BeNullOrEmpty + $Error.Count | Should -Not -Be 0 + $Error[0].FullyQualifiedErrorId | Should -Be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + +### TODO: FIX this text + It "Should not save resource with incorrectly formatted version such as version formatted with invalid delimiter [1-0-0-0]"{ + $Version = "[1-0-0-0]" + + try { + Save-PSResource -Name $testModuleName -Version $Version -Repository $AzureArtifactsName -Path $SaveDir -ErrorAction SilentlyContinue -TrustRepository + } + catch + {} + + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -BeNullOrEmpty + $Error.Count | Should -Not -Be 0 + $Error[0].FullyQualifiedErrorId | Should -BeExactly "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Save resource with latest (including prerelease) version given Prerelease parameter" { + Save-PSResource -Name $testModuleName -Prerelease -Repository $AzureArtifactsName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "5.2.5" + } + + It "Save PSResourceInfo object piped in for prerelease version object" { + Find-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $AzureArtifactsName | Save-PSResource -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem -Path $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save module as a nupkg" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $AzureArtifactsName -Path $SaveDir -AsNupkg -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "test_module.1.0.0.nupkg" + $pkgDir | Should -Not -BeNullOrEmpty + } + + It "Save module and include XML metadata file" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $AzureArtifactsName -Path $SaveDir -IncludeXml -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + $xmlFile = Get-ChildItem -Path $pkgDirVersion.FullName | Where-Object Name -eq "PSGetModuleInfo.xml" + $xmlFile | Should -Not -BeNullOrEmpty + } + + It "Save module using -PassThru" { + $res = Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $AzureArtifactsName -Path $SaveDir -PassThru -TrustRepository + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "1.0.0" + } + + # Save module that is not authenticode signed + # Should FAIL to save the module + It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { + { Save-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $AzureArtifactsName -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } +} +#> \ No newline at end of file diff --git a/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 b/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 new file mode 100644 index 000000000..63071cf0d --- /dev/null +++ b/test/SavePSResourceTests/SavePSResourceLocalTests.ps1 @@ -0,0 +1,140 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Save-PSResource for local repositories' { + + BeforeAll { + $localRepo = "psgettestlocal" + $moduleName = "test_local_mod" + $moduleName2 = "test_local_mod2" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "1.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "3.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "5.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName2 $localRepo "5.0.0" + + + $SaveDir = Join-Path $TestDrive 'SavedResources' + New-Item -Item Directory $SaveDir -Force + } + + AfterEach { + # Delete contents of save directory + Remove-Item -Path (Join-Path $SaveDir '*') -Recurse -Force -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Save specific module resource by name" { + Save-PSResource -Name $moduleName -Repository $localRepo -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save multiple resources by name" { + $pkgNames = @($moduleName, $moduleName2) + Save-PSResource -Name $pkgNames -Repository $localRepo -Path $SaveDir -TrustRepository + $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq $moduleName -or $_.Name -eq $moduleName2 } + $pkgDirs.Count | Should -Be 2 + (Get-ChildItem $pkgDirs[0].FullName).Count | Should -Be 1 + (Get-ChildItem $pkgDirs[1].FullName).Count | Should -Be 1 + } + + It "Should not save resource given nonexistant name" { + Save-PSResource -Name NonExistentModule -Repository $localRepo -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "NonExistentModule" + $pkgDir.Name | Should -BeNullOrEmpty + } + + It "Should save resource given name and exact version" { + Save-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + } + + It "Should save resource given name and exact version with bracket syntax" { + Save-PSResource -Name $moduleName -Version "[1.0.0]" -Repository $localRepo -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + } + +# It "Should save resource given name and exact range inclusive [1.0.0, 3.0.0]" { +# Save-PSResource -Name $moduleName -Version "[1.0.0, 3.0.0]" -Repository $localRepo -Path $SaveDir -TrustRepository +# $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName +# $pkgDir | Should -Not -BeNullOrEmpty +# $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName +# $pkgDirVersion.Name | Should -Be "3.0.0" +# } + + It "Should save resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Save-PSResource -Name $moduleName -Version "(1.0.0, 5.0.0)" -Repository $localRepo -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0" + } + + It "Should not save resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version="(1.0.0.0)" + try { + Save-PSResource -Name $moduleName -Version $Version -Repository $localRepo -Path $SaveDir -ErrorAction SilentlyContinue -TrustRepository + } + catch + {} + + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -BeNullOrEmpty + $Error.Count | Should -Not -Be 0 + $Error[0].FullyQualifiedErrorId | Should -Be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Save PSResourceInfo object piped in for prerelease version object" { + Find-PSResource -Name $moduleName -Version "5.0.0" -Repository $localRepo | Save-PSResource -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem -Path $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save module as a nupkg" { + Save-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -Path $SaveDir -AsNupkg -TrustRepository -WarningVariable WarningVar -warningaction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + # not yet implemented + # $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "$moduleName.1.0.0.nupkg" + # $pkgDir | Should -Not -BeNullOrEmpty + } + + It "Save module and include XML metadata file" { + Save-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -Path $SaveDir -IncludeXml -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $moduleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + $xmlFile = Get-ChildItem -Path $pkgDirVersion.FullName | Where-Object Name -eq "PSGetModuleInfo.xml" + $xmlFile | Should -Not -BeNullOrEmpty + } + + It "Save module using -PassThru" { + $res = Save-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -Path $SaveDir -PassThru -TrustRepository + $res.Name | Should -Be $moduleName + $res.Version | Should -Be "1.0.0.0" + } + + # Save module that is not authenticode signed + # Should FAIL to save the module + It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { + { Save-PSResource -Name $moduleName -Version "5.0.0" -AuthenticodeCheck -Repository $localRepo -TrustRepository -Path $SaveDir -ErrorAction SilentlyContinue } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + #> +} \ No newline at end of file diff --git a/test/SavePSResourceTests/SavePSResourceV2Tests.ps1 b/test/SavePSResourceTests/SavePSResourceV2Tests.ps1 new file mode 100644 index 000000000..daba6ce8c --- /dev/null +++ b/test/SavePSResourceTests/SavePSResourceV2Tests.ps1 @@ -0,0 +1,172 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Save-PSResource for V2 Server Protocol' { + + BeforeAll { + $PSGalleryName = Get-PSGalleryName + $testModuleName = "test_module" + $testScriptName = "test_script" + $testModuleName2 = "testmodule99" + $PackageManagement = "PackageManagement" + Get-NewPSResourceRepositoryFile + + $SaveDir = Join-Path $TestDrive 'SavedResources' + New-Item -Item Directory $SaveDir -Force + } + + AfterEach { + # Delete contents of save directory + Remove-Item -Path (Join-Path $SaveDir '*') -Recurse -Force -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Save specific module resource by name" { + Save-PSResource -Name $testModuleName -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save specific script resource by name" { + Save-PSResource -Name $testScriptName -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "test_script.ps1" + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save multiple resources by name" { + $pkgNames = @($testModuleName, $testModuleName2) + Save-PSResource -Name $pkgNames -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq $testModuleName -or $_.Name -eq $testModuleName2 } + $pkgDirs.Count | Should -Be 2 + (Get-ChildItem $pkgDirs[0].FullName).Count | Should -Be 1 + (Get-ChildItem $pkgDirs[1].FullName).Count | Should -Be 1 + } + + It "Should not save resource given nonexistant name" { + Save-PSResource -Name NonExistentModule -Repository $PSGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "NonExistentModule" + $pkgDir.Name | Should -BeNullOrEmpty + } + + It "Not Save module with Name containing wildcard" { + Save-PSResource -Name "TestModule*" -Repository $PSGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "NameContainsWildcard,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Should save resource given name and exact version" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0.0" + } + + It "Should save resource given name and exact version with bracket syntax" { + Save-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0.0" + } + + It "Should save resource given name and exact range inclusive [1.0.0, 3.0.0]" { + Save-PSResource -Name $testModuleName -Version "[1.0.0, 3.0.0]" -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0.0" + } + + It "Should save resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Save-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0.0" + } + + It "Should not save resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version="(1.0.0.0)" + try { + Save-PSResource -Name $testModuleName -Version $Version -Repository $PSGalleryName -Path $SaveDir -ErrorAction SilentlyContinue -TrustRepository + } + catch + {} + + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -BeNullOrEmpty + $Error.Count | Should -Not -Be 0 + $Error[0].FullyQualifiedErrorId | Should -Be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Save resource with latest (including prerelease) version given Prerelease parameter" { + Save-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "5.2.5" + } + + It "Save a module with a dependency" { + Save-PSResource -Name "TestModuleWithDependencyE" -Version "1.0.0.0" -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq "TestModuleWithDependencyE" -or $_.Name -eq "TestModuleWithDependencyC" -or $_.Name -eq "TestModuleWithDependencyB" -or $_.Name -eq "TestModuleWithDependencyD"} + $pkgDirs.Count | Should -BeGreaterThan 1 + (Get-ChildItem $pkgDirs[0].FullName).Count | Should -BeGreaterThan 0 + (Get-ChildItem $pkgDirs[1].FullName).Count | Should -BeGreaterThan 0 + (Get-ChildItem $pkgDirs[2].FullName).Count | Should -BeGreaterThan 0 + (Get-ChildItem $pkgDirs[3].FullName).Count | Should -BeGreaterThan 0 + } + + It "Save a module with a dependency and skip saving the dependency" { + Save-PSResource -Name "TestModuleWithDependencyE" -Version "1.0.0.0" -SkipDependencyCheck -Repository $PSGalleryName -Path $SaveDir -TrustRepository + $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq "TestModuleWithDependencyE"} + $pkgDirs.Count | Should -Be 1 + (Get-ChildItem $pkgDirs[0].FullName).Count | Should -Be 1 + } + + It "Save PSResourceInfo object piped in for prerelease version object" { + Find-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $PSGalleryName | Save-PSResource -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem -Path $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save module as a nupkg" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -Path $SaveDir -AsNupkg -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "test_module.1.0.0.nupkg" + $pkgDir | Should -Not -BeNullOrEmpty + } + + It "Save module and include XML metadata file" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -Path $SaveDir -IncludeXml -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0.0" + $xmlFile = Get-ChildItem -Path $pkgDirVersion.FullName | Where-Object Name -eq "PSGetModuleInfo.xml" + $xmlFile | Should -Not -BeNullOrEmpty + } + + It "Save module using -PassThru" { + $res = Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $PSGalleryName -Path $SaveDir -PassThru -TrustRepository + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "1.0.0.0" + } + + # Save module that is not authenticode signed + # Should FAIL to save the module + It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { + Save-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } +} \ No newline at end of file diff --git a/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 b/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 new file mode 100644 index 000000000..945685c3c --- /dev/null +++ b/test/SavePSResourceTests/SavePSResourceV3Tests.ps1 @@ -0,0 +1,146 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Save-PSResource for V3 Server Protocol' { + + BeforeAll { + $NuGetGalleryName = Get-NuGetGalleryName + $testModuleName = "test_module" + $testModuleName2 = "test_module2" + Get-NewPSResourceRepositoryFile + + $SaveDir = Join-Path $TestDrive 'SavedResources' + New-Item -Item Directory $SaveDir -Force + } + + AfterEach { + # Delete contents of save directory + Remove-Item -Path (Join-Path $SaveDir '*') -Recurse -Force -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Save specific module resource by name" { + Save-PSResource -Name $testModuleName -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save multiple resources by name" { + $pkgNames = @($testModuleName, $testModuleName2) + Save-PSResource -Name $pkgNames -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDirs = Get-ChildItem -Path $SaveDir | Where-Object { $_.Name -eq $testModuleName -or $_.Name -eq $testModuleName2 } + $pkgDirs.Count | Should -Be 2 + (Get-ChildItem $pkgDirs[0].FullName).Count | Should -Be 1 + (Get-ChildItem $pkgDirs[1].FullName).Count | Should -Be 1 + } + + It "Should not save resource given nonexistant name" { + Save-PSResource -Name NonExistentModule -Repository $NuGetGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "NonExistentModule" + $pkgDir.Name | Should -BeNullOrEmpty + } + + It "Not Save module with Name containing wildcard" { + Save-PSResource -Name "TestModule*" -Repository $NuGetGalleryName -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue -TrustRepository + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "NameContainsWildcard,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Should save resource given name and exact version" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + } + + It "Should save resource given name and exact version with bracket syntax" { + Save-PSResource -Name $testModuleName -Version "[1.0.0]" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + } + + It "Should save resource given name and exact range inclusive [1.0.0, 3.0.0]" { + Save-PSResource -Name $testModuleName -Version "[1.0.0, 3.0.0]" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0" + } + + It "Should save resource given name and exact range exclusive (1.0.0, 5.0.0)" { + Save-PSResource -Name $testModuleName -Version "(1.0.0, 5.0.0)" -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "3.0.0" + } + + It "Should not save resource with incorrectly formatted version such as exclusive version (1.0.0.0)" { + $Version="(1.0.0.0)" + try { + Save-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -Path $SaveDir -ErrorAction SilentlyContinue -TrustRepository + } + catch + {} + + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -BeNullOrEmpty + $Error.Count | Should -Not -Be 0 + $Error[0].FullyQualifiedErrorId | Should -Be "IncorrectVersionFormat,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } + + It "Save resource with latest (including prerelease) version given Prerelease parameter" { + Save-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "5.2.5" + } + + It "Save PSResourceInfo object piped in for prerelease version object" { + Find-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $NuGetGalleryName | Save-PSResource -Path $SaveDir -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + (Get-ChildItem -Path $pkgDir.FullName).Count | Should -Be 1 + } + + It "Save module as a nupkg" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -AsNupkg -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq "test_module.1.0.0.nupkg" + $pkgDir | Should -Not -BeNullOrEmpty + } + + It "Save module and include XML metadata file" { + Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -IncludeXml -TrustRepository + $pkgDir = Get-ChildItem -Path $SaveDir | Where-Object Name -eq $testModuleName + $pkgDir | Should -Not -BeNullOrEmpty + $pkgDirVersion = Get-ChildItem -Path $pkgDir.FullName + $pkgDirVersion.Name | Should -Be "1.0.0" + $xmlFile = Get-ChildItem -Path $pkgDirVersion.FullName | Where-Object Name -eq "PSGetModuleInfo.xml" + $xmlFile | Should -Not -BeNullOrEmpty + } + + It "Save module using -PassThru" { + $res = Save-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -Path $SaveDir -PassThru -TrustRepository + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "1.0.0" + } + + # Save module that is not authenticode signed + # Should FAIL to save the module + It "Save module that is not authenticode signed" -Skip:(!(Get-IsWindows)) { + Save-PSResource -Name $testModuleName -Version "5.0.0" -AuthenticodeCheck -Repository $NuGetGalleryName -TrustRepository -Path $SaveDir -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.SavePSResource" + } +} \ No newline at end of file diff --git a/test/UpdatePSResourceTests/UpdatePSResourceAATests.ps1 b/test/UpdatePSResourceTests/UpdatePSResourceAATests.ps1 new file mode 100644 index 000000000..be0e1c27e --- /dev/null +++ b/test/UpdatePSResourceTests/UpdatePSResourceAATests.ps1 @@ -0,0 +1,296 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# WIP +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Update-PSResource for Azure Artifacts' { + + BeforeAll{ + $AzureArtifactsName = "AzureArtifacts" + $testModuleName = "test_module" + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $AzureArtifactsName -Uri "https://pkgs.dev.azure.com/PowerShellGetTesting/PSGetTesting/_packaging/PSGetTest/nuget/v3/index.json" + + $SaveDir = Join-Path $TestDrive 'SavedResources' + New-Item -Item Directory $SaveDir -Force + + AfterEach { + Uninstall-PSResource $testModuleName -Version "*" + } + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Update resource installed given Name parameter" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + + Update-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + It "Update resource installed given Name and Version (specific) parameters" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + + Update-PSResource -Name $testModuleName -Version "5.0.0.0" -Repository $NuGetGalleryName -TrustRepository + $res = Get-PSResource -Name $testModuleName + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -eq [System.Version]"5.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -BeTrue + } + + $testCases2 = @{Version="[3.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, exact match"}, + @{Version="3.0.0"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, exact match without bracket syntax"}, + @{Version="[3.0.0, 5.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, exact range inclusive"}, + @{Version="(3.0.0, 6.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, exact range exclusive"}, + @{Version="(3.0.0,)"; ExpectedVersions=@("1.0.0", "5.0.0"); Reason="validate version, minimum version exclusive"}, + @{Version="[3.0.0,)"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, minimum version inclusive"}, + @{Version="(,5.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, maximum version exclusive"}, + @{Version="(,5.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, maximum version inclusive"}, + @{Version="[1.0.0, 5.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} + @{Version="(1.0.0, 3.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} + + It "Update resource when given Name to " -TestCases $testCases2{ + param($Version, $ExpectedVersions) + + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -TrustRepository + + $res = Get-PSResource -Name $testModuleName + + foreach ($item in $res) { + $item.Name | Should -Be $testModuleName + $ExpectedVersions | Should -Contain $item.Version + } + } + + $testCases = @( + @{Version='(3.0.0.0)'; Description="exclusive version (3.0.0.0)"}, + @{Version='[3-0-0-0]'; Description="version formatted with invalid delimiter [3-0-0-0]"} + ) + It "Should not update resource with incorrectly formatted version such as " -TestCases $testCases{ + param($Version, $Description) + + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -TrustRepository 2>$null + + $res = Get-PSResource -Name $testModuleName + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $false + } + + It "Update resource with latest (including prerelease) version given Prerelease parameter" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName -TrustRepository + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -ge [System.Version]"5.2.5") + { + $pkg.Prerelease | Should -Be "alpha001" + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Windows only + It "update resource under CurrentUser scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + # TODO: perhaps also install TestModule with the highest version (the one above 1.2.0.0) to the AllUsers path too + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Windows only + It "update resource under AllUsers scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name "testmodule99" -Version "0.0.93" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers + + $res = Get-Module -Name "testmodule99" -ListAvailable + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Contain "0.0.93" + } + + # Windows only + It "Update resource under no specified scope" -skip:(!$IsWindows) { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $NuGetGalleryName -TrustRepository -verbose + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + # this line is commented out because AllUsers scope requires sudo and that isn't supported in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + # this test is skipped because it requires sudo to run and has yet to be resolved in CI + It "Update resource under AllUsers scope - Unix only" -Skip:($true) { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope AllUsers + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("usr") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + # this is commented out because it requires sudo to run with AllUsers scope and this hasn't been resolved in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # It "update resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "TestModuleWithLicense" -Version "0.0.1.0" -Repository $TestGalleryName -AcceptLicense + # Update-PSResource -Name "TestModuleWithLicense" -Repository $TestGalleryName -AcceptLicense + # $res = Get-PSResource "TestModuleWithLicense" + + # $isPkgUpdated = $false + # foreach ($pkg in $res) + # { + # if ([System.Version]$pkg.Version -gt [System.Version]"0.0.1.0") + # { + # $isPkgUpdated = $true + # } + # } + + # $isPkgUpdated | Should -Be $true + # } + + It "Update module using -WhatIf, should not update the module" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -WhatIf -Repository $NuGetGalleryName -TrustRepository + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $false + } + + It "Update resource installed given -Name and -PassThru parameters" { + Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -TrustRepository + + $res = Update-PSResource -Name $testModuleName -Version "3.0.0" -Repository $NuGetGalleryName -TrustRepository -PassThru + $res.Name | Should -Contain $testModuleName + $res.Version | Should -Contain "3.0.0" + } +} +#> \ No newline at end of file diff --git a/test/UpdatePSResourceTests/UpdatePSResourceLocalTests.ps1 b/test/UpdatePSResourceTests/UpdatePSResourceLocalTests.ps1 new file mode 100644 index 000000000..fbf6344b8 --- /dev/null +++ b/test/UpdatePSResourceTests/UpdatePSResourceLocalTests.ps1 @@ -0,0 +1,275 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test Update-PSResource for local repositories' { + + + BeforeAll { + $localRepo = "psgettestlocal" + $moduleName = "test_local_mod" + $moduleName2 = "test_local_mod2" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "1.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "3.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName $localRepo "5.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName2 $localRepo "1.0.0" + Get-ModuleResourcePublishedToLocalRepoTestDrive $moduleName2 $localRepo "5.0.0" + } + + AfterEach { + Uninstall-PSResource $moduleName, $moduleName2 -Version "*" + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Update resource installed given Name parameter" { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + It "Update resources installed given Name (with wildcard) parameter" { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository + Install-PSResource -Name $moduleName2 -Version "1.0.0" -Repository $localRepo -TrustRepository + + Update-PSResource -Name "test_local*" -Repository $localRepo -TrustRepository + $res = Get-PSResource -Name "test_local*" -Version "5.0.0" + + $inputHashtable = @{test_module = "1.0.0"; test_module2 = "1.0.0"} + $isTest_ModuleUpdated = $false + $isTest_Module2Updated = $false + foreach ($item in $res) + { + if ([System.Version]$item.Version -gt [System.Version]$inputHashtable[$item.Name]) + { + if ($item.Name -like $moduleName) + { + $isTest_ModuleUpdated = $true + } + elseif ($item.Name -like $moduleName2) + { + $isTest_Module2Updated = $true + } + } + } + + $isTest_ModuleUpdated | Should -BeTrue + $isTest_Module2Updated | Should -BeTrue + } + + It "Update resource installed given Name and Version (specific) parameters" { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository + + Update-PSResource -Name $moduleName -Version "5.0.0" -Repository $localRepo -TrustRepository + $res = Get-PSResource -Name $moduleName + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -eq [System.Version]"5.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -BeTrue + } + + # Windows only + It "update resource under CurrentUser scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + # TODO: perhaps also install TestModule with the highest version (the one above 1.2.0.0) to the AllUsers path too + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope AllUsers + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $moduleName -Version "3.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Windows only + It "update resource under AllUsers scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + Install-PSResource -Name $moduleName -Version "1.0.0" -Repository $localRepo -TrustRepository -Scope AllUsers + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository -Scope AllUsers + + $res = Get-Module -Name $moduleName -ListAvailable + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Contain "5.0.0" + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.ModuleBase.Contains("Program") | Should -Be $true + $isPkgUpdated = $true + } + } + $isPkgUpdated | Should -Be $true + + } + + # Windows only + It "Update resource under no specified scope" -skip:(!$IsWindows) { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository + Update-PSResource -Name $moduleName -Version "5.0.0.0" -Repository $localRepo -TrustRepository + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + # this line is commented out because AllUsers scope requires sudo and that isn't supported in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + # this test is skipped because it requires sudo to run and has yet to be resolved in CI + It "Update resource under AllUsers scope - Unix only" -Skip:($true) { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope AllUsers + + Update-PSResource -Name $moduleName -Repository $PSGalleryName -TrustRepository -Scope AllUsers + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("usr") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + # this is commented out because it requires sudo to run with AllUsers scope and this hasn't been resolved in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $moduleName -Repository $localRepo -TrustRepository + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # It "update resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "TestModuleWithLicense" -Version "0.0.1.0" -Repository $TestGalleryName -AcceptLicense + # Update-PSResource -Name "TestModuleWithLicense" -Repository $TestGalleryName -AcceptLicense + # $res = Get-PSResource "TestModuleWithLicense" + + # $isPkgUpdated = $false + # foreach ($pkg in $res) + # { + # if ([System.Version]$pkg.Version -gt [System.Version]"0.0.1.0") + # { + # $isPkgUpdated = $true + # } + # } + + # $isPkgUpdated | Should -Be $true + # } + + It "Update module using -WhatIf, should not update the module" { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository + Update-PSResource -Name $moduleName -WhatIf -Repository $localRepo -TrustRepository + + $res = Get-PSResource -Name $moduleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $false + } + + It "Update resource installed given -Name and -PassThru parameters" { + Install-PSResource -Name $moduleName -Version "1.0.0.0" -Repository $localRepo -TrustRepository + + $res = Update-PSResource -Name $moduleName -Version "5.0.0.0" -Repository $localRepo -TrustRepository -PassThru + $res.Name | Should -Contain $moduleName + $res.Version | Should -Be "5.0.0.0" + } +} diff --git a/test/UpdatePSResource.Tests.ps1 b/test/UpdatePSResourceTests/UpdatePSResourceV2Tests.ps1 similarity index 86% rename from test/UpdatePSResource.Tests.ps1 rename to test/UpdatePSResourceTests/UpdatePSResourceV2Tests.ps1 index b0c45c17d..5ddb55bb9 100644 --- a/test/UpdatePSResource.Tests.ps1 +++ b/test/UpdatePSResourceTests/UpdatePSResourceV2Tests.ps1 @@ -2,9 +2,9 @@ # Licensed under the MIT License. $ProgressPreference = "SilentlyContinue" -Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force -Describe 'Test Update-PSResource' { +Describe 'Test HTTP Update-PSResource for V2 Server Protocol' { BeforeAll { @@ -19,16 +19,16 @@ Describe 'Test Update-PSResource' { } AfterEach { - Uninstall-PSResource "test_module", "TestModule99", "TestModuleWithLicense", "test_module2", "test_script", "PackaeManagement" -Version "*" + Uninstall-PSResource "test_module", "TestModule99", "TestModuleWithLicense", "test_module2", "test_script" -Version "*" } AfterAll { Get-RevertPSResourceRepositoryFile } - It "update resource installed given Name parameter" { + It "Update resource installed given Name parameter" { Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository - + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository $res = Get-PSResource -Name $testModuleName @@ -44,7 +44,7 @@ Describe 'Test Update-PSResource' { $isPkgUpdated | Should -Be $true } - It "update resources installed given Name (with wildcard) parameter" { + It "Update resources installed given Name (with wildcard) parameter" { Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository Install-PSResource -Name $testModuleName2 -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository @@ -73,7 +73,7 @@ Describe 'Test Update-PSResource' { $isTest_Module2Updated | Should -BeTrue } - It "update resource installed given Name and Version (specific) parameters" { + It "Update resource installed given Name and Version (specific) parameters" { Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name $testModuleName -Version "5.0.0.0" -Repository $PSGalleryName -TrustRepository @@ -101,7 +101,7 @@ Describe 'Test Update-PSResource' { @{Version="[1.0.0.0, 5.0.0.0)"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} @{Version="(1.0.0.0, 3.0.0.0]"; ExpectedVersions=@("1.0.0.0", "3.0.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} - It "update resource when given Name to " -TestCases $testCases2{ + It "Update resource when given Name to " -TestCases $testCases2{ param($Version, $ExpectedVersions) Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository @@ -138,7 +138,7 @@ Describe 'Test Update-PSResource' { $isPkgUpdated | Should -Be $false } - It "update resource with latest (including prerelease) version given Prerelease parameter" { + It "Update resource with latest (including prerelease) version given Prerelease parameter" { Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository Update-PSResource -Name $testModuleName -Prerelease -Repository $PSGalleryName -TrustRepository $res = Get-PSResource -Name $testModuleName @@ -156,8 +156,8 @@ Describe 'Test Update-PSResource' { $isPkgUpdated | Should -Be $true } - # Windows only - It "update resource under CurrentUser scope" -skip:(!$IsWindows) { + # Windows only + It "update resource under CurrentUser scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { # TODO: perhaps also install TestModule with the highest version (the one above 1.2.0.0) to the AllUsers path too Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope AllUsers Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser @@ -178,13 +178,13 @@ Describe 'Test Update-PSResource' { $isPkgUpdated | Should -Be $true } - + # Windows only It "update resource under AllUsers scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { - Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $PSGalleryName -TrustRepository -Scope AllUsers -Verbose - Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser -Verbose + Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $PSGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser - Update-PSResource -Name "testmodule99" -Version "0.0.93" -Repository $PSGalleryName -TrustRepository -Scope AllUsers -Verbose + Update-PSResource -Name "testmodule99" -Version "0.0.93" -Repository $PSGalleryName -TrustRepository -Scope AllUsers $res = Get-Module -Name "testmodule99" -ListAvailable $res | Should -Not -BeNullOrEmpty @@ -192,9 +192,9 @@ Describe 'Test Update-PSResource' { } # Windows only - It "update resource under no specified scope" -skip:(!$IsWindows) { + It "Update resource under no specified scope" -skip:(!$IsWindows) { Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $PSGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $PSGalleryName -TrustRepository -verbose $res = Get-PSResource -Name $testModuleName @@ -318,7 +318,7 @@ Describe 'Test Update-PSResource' { $isPkgUpdated | Should -Be $false } - It "update resource installed given -Name and -PassThru parameters" { + It "Update resource installed given -Name and -PassThru parameters" { Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository $res = Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $PSGalleryName -TrustRepository -PassThru @@ -326,17 +326,27 @@ Describe 'Test Update-PSResource' { $res.Version | Should -Contain "3.0.0.0" } + ## TODO update this -- find module with valid catalog file # Update to module 1.4.3 (is authenticode signed and has catalog file) - # Should update successfully + # Should update successfully It "Update module with catalog file using publisher validation" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - Update-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository + Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository -verbose + Update-PSResource -Name $PackageManagement -Version "1.4.3" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository $res1 = Get-PSResource $PackageManagement -Version "1.4.3" $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.3.0" + $res1.Version | Should -Be "1.4.3" } + # # Update to module 1.4.4.1 (with incorrect catalog file) + # # Should FAIL to update the module + # It "Update module with incorrect catalog file" -Skip:(!(Get-IsWindows)) { + # Install-PSResource -Name $PackageManagement -Version "1.4.2" -Reinstall -Repository $PSGalleryName -TrustRepository + # Update-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + # $err.Count | Should -Not -Be 0 + # $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" + # } + # Update to module 1.4.7 (is authenticode signed and has NO catalog file) # Should update successfully It "Install module with no catalog file" -Skip:(!(Get-IsWindows)) { @@ -345,14 +355,7 @@ Describe 'Test Update-PSResource' { $res1 = Get-PSResource $PackageManagement -Version "1.4.7" $res1.Name | Should -Be $PackageManagement - $res1.Version | Should -Be "1.4.7.0" - } - - # Update to module 1.4.4.1 (with incorrect catalog file) - # Should FAIL to update the module - It "Update module with incorrect catalog file" -Skip:(!(Get-IsWindows)) { - Install-PSResource -Name $PackageManagement -Version "1.4.2" -Repository $PSGalleryName -TrustRepository - { Update-PSResource -Name $PackageManagement -Version "1.4.4.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "TestFileCatalogError,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" + $res1.Version | Should -Be "1.4.7" } # Update script that is signed @@ -363,13 +366,15 @@ Describe 'Test Update-PSResource' { $res1 = Get-PSResource "Install-VSCode" -Version "1.4.2" $res1.Name | Should -Be "Install-VSCode" - $res1.Version | Should -Be "1.4.2.0" + $res1.Version | Should -Be "1.4.2" } # Update script that is not signed # Should throw It "Update script that is not signed" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name "TestTestScript" -Version "1.0" -Repository $PSGalleryName -TrustRepository - { Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository } | Should -Throw -ErrorId "GetAuthenticodeSignatureError,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" + Update-PSResource -Name "TestTestScript" -Version "1.3.1.1" -AuthenticodeCheck -Repository $PSGalleryName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -Not -Be 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PowerShellGet.Cmdlets.UpdatePSResource" } } diff --git a/test/UpdatePSResourceTests/UpdatePSResourceV3Tests.ps1 b/test/UpdatePSResourceTests/UpdatePSResourceV3Tests.ps1 new file mode 100644 index 000000000..41f81fb33 --- /dev/null +++ b/test/UpdatePSResourceTests/UpdatePSResourceV3Tests.ps1 @@ -0,0 +1,291 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$((Get-Item $psscriptroot).parent)\PSGetTestUtils.psm1" -Force + +Describe 'Test HTTP Update-PSResource for V3 Server Protocol' { + + BeforeAll{ + $NuGetGalleryName = Get-NuGetGalleryName + $testModuleName = "test_module" + Get-NewPSResourceRepositoryFile + } + + AfterEach { + Uninstall-PSResource $testModuleName -Version "*" + } + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "Update resource installed given Name parameter" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + + Update-PSResource -Name $testModuleName -Repository $NuGetGalleryName -TrustRepository + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + It "Update resource installed given Name and Version (specific) parameters" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + + Update-PSResource -Name $testModuleName -Version "5.0.0.0" -Repository $NuGetGalleryName -TrustRepository + $res = Get-PSResource -Name $testModuleName + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -eq [System.Version]"5.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -BeTrue + } + + $testCases2 = @{Version="[3.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, exact match"}, + @{Version="3.0.0"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, exact match without bracket syntax"}, + @{Version="[3.0.0, 5.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, exact range inclusive"}, + @{Version="(3.0.0, 6.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, exact range exclusive"}, + @{Version="(3.0.0,)"; ExpectedVersions=@("1.0.0", "5.0.0"); Reason="validate version, minimum version exclusive"}, + @{Version="[3.0.0,)"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, minimum version inclusive"}, + @{Version="(,5.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, maximum version exclusive"}, + @{Version="(,5.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0", "5.0.0"); Reason="validate version, maximum version inclusive"}, + @{Version="[1.0.0, 5.0.0)"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, mixed inclusive minimum and exclusive maximum version"} + @{Version="(1.0.0, 3.0.0]"; ExpectedVersions=@("1.0.0", "3.0.0"); Reason="validate version, mixed exclusive minimum and inclusive maximum version"} + + It "Update resource when given Name to " -TestCases $testCases2{ + param($Version, $ExpectedVersions) + + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -TrustRepository + + $res = Get-PSResource -Name $testModuleName + + foreach ($item in $res) { + $item.Name | Should -Be $testModuleName + $ExpectedVersions | Should -Contain $item.Version + } + } + + $testCases = @( + @{Version='(3.0.0.0)'; Description="exclusive version (3.0.0.0)"}, + @{Version='[3-0-0-0]'; Description="version formatted with invalid delimiter [3-0-0-0]"} + ) + It "Should not update resource with incorrectly formatted version such as " -TestCases $testCases{ + param($Version, $Description) + + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version $Version -Repository $NuGetGalleryName -TrustRepository 2>$null + + $res = Get-PSResource -Name $testModuleName + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $false + } + + It "Update resource with latest (including prerelease) version given Prerelease parameter" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Prerelease -Repository $NuGetGalleryName -TrustRepository + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -ge [System.Version]"5.2.5") + { + $pkg.Prerelease | Should -Be "alpha001" + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Windows only + It "update resource under CurrentUser scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + # TODO: perhaps also install TestModule with the highest version (the one above 1.2.0.0) to the AllUsers path too + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Windows only + It "update resource under AllUsers scope" -skip:(!($IsWindows -and (Test-IsAdmin))) { + Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name "testmodule99" -Version "0.0.91" -Repository $NuGetGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name "testmodule99" -Version "0.0.93" -Repository $NuGetGalleryName -TrustRepository -Scope AllUsers + + $res = Get-Module -Name "testmodule99" -ListAvailable + $res | Should -Not -BeNullOrEmpty + $res.Version | Should -Contain "0.0.93" + } + + # Windows only + It "Update resource under no specified scope" -skip:(!$IsWindows) { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -Version "3.0.0.0" -Repository $NuGetGalleryName -TrustRepository -verbose + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("Documents") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under CurrentUser scope - Unix only" -Skip:(Get-IsWindows) { + # this line is commented out because AllUsers scope requires sudo and that isn't supported in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/usr/local/share/powershell/Modules' + # this test is skipped because it requires sudo to run and has yet to be resolved in CI + It "Update resource under AllUsers scope - Unix only" -Skip:($true) { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope AllUsers + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("usr") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # Unix only + # Expected path should be similar to: '/home/janelane/.local/share/powershell/Modules' + It "Update resource under no specified scope - Unix only" -Skip:(Get-IsWindows) { + # this is commented out because it requires sudo to run with AllUsers scope and this hasn't been resolved in CI yet + # Install-PSResource -Name "TestModule" -Version "1.1.0.0" -Repository $TestGalleryName -Scope AllUsers + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $PSGalleryName -TrustRepository -Scope CurrentUser + + Update-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $pkg.InstalledLocation.Contains("$env:HOME/.local") | Should -Be $true + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $true + } + + # It "update resource that requires accept license with -AcceptLicense flag" { + # Install-PSResource -Name "TestModuleWithLicense" -Version "0.0.1.0" -Repository $TestGalleryName -AcceptLicense + # Update-PSResource -Name "TestModuleWithLicense" -Repository $TestGalleryName -AcceptLicense + # $res = Get-PSResource "TestModuleWithLicense" + + # $isPkgUpdated = $false + # foreach ($pkg in $res) + # { + # if ([System.Version]$pkg.Version -gt [System.Version]"0.0.1.0") + # { + # $isPkgUpdated = $true + # } + # } + + # $isPkgUpdated | Should -Be $true + # } + + It "Update module using -WhatIf, should not update the module" { + Install-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName -TrustRepository + Update-PSResource -Name $testModuleName -WhatIf -Repository $NuGetGalleryName -TrustRepository + + $res = Get-PSResource -Name $testModuleName + + $isPkgUpdated = $false + foreach ($pkg in $res) + { + if ([System.Version]$pkg.Version -gt [System.Version]"1.0.0.0") + { + $isPkgUpdated = $true + } + } + + $isPkgUpdated | Should -Be $false + } + + It "Update resource installed given -Name and -PassThru parameters" { + Install-PSResource -Name $testModuleName -Version "1.0.0" -Repository $NuGetGalleryName -TrustRepository + + $res = Update-PSResource -Name $testModuleName -Version "3.0.0" -Repository $NuGetGalleryName -TrustRepository -PassThru + $res.Name | Should -Contain $testModuleName + $res.Version | Should -Contain "3.0.0" + } +} diff --git a/test/testRepositories.xml b/test/testRepositories.xml index a0da40886..0b1895050 100644 --- a/test/testRepositories.xml +++ b/test/testRepositories.xml @@ -1,5 +1,5 @@ - - + +