From 21e6dbf57d401038fc67995a69eabcdf0073ffdf Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 31 May 2023 12:18:39 -0400 Subject: [PATCH 1/5] apply changes for ADO support to V3Server existing support --- src/code/PSResourceInfo.cs | 12 +- src/code/RepositorySettings.cs | 6 +- src/code/ResponseUtilFactory.cs | 1 + src/code/V3ServerAPICalls.cs | 1312 ++++++++--------- .../FindPSResourceADOServer.Tests.ps1 | 213 +++ .../InstallPSResourceADOServer.Tests.ps1 | 264 ++++ .../InstallPSResourceLocal.Tests.ps1 | 4 +- .../InstallPSResourceV3Server.Tests.ps1 | 27 +- 8 files changed, 1156 insertions(+), 683 deletions(-) create mode 100644 test/FindPSResourceTests/FindPSResourceADOServer.Tests.ps1 create mode 100644 test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index ab854464a..95690fbe1 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -622,6 +622,8 @@ public static bool TryConvertFromJson( string versionValue = versionElement.ToString(); metadata["Version"] = ParseHttpVersion(versionValue, out string prereleaseLabel); metadata["Prerelease"] = prereleaseLabel; + // ADO server response does not contain "isPrerelease" element, so we set it here. + metadata["IsPrerelease"] = !String.IsNullOrEmpty(prereleaseLabel); if (!NuGetVersion.TryParse(versionValue, out NuGetVersion parsedNormalizedVersion)) { @@ -631,6 +633,7 @@ public static bool TryConvertFromJson( parsedNormalizedVersion = new NuGetVersion("1.0.0.0"); } + metadata["NormalizedVersion"] = parsedNormalizedVersion; } @@ -646,6 +649,12 @@ public static bool TryConvertFromJson( metadata["ProjectUrl"] = ParseHttpUrl(projectUrlElement.ToString()) as Uri; } + // Icon Url + if (rootDom.TryGetProperty("iconUrl", out JsonElement iconUrlElement)) + { + metadata["IconUrl"] = ParseHttpUrl(iconUrlElement.ToString()) as Uri; + } + // Tags if (rootDom.TryGetProperty("tags", out JsonElement tagsElement)) { @@ -664,9 +673,10 @@ public static bool TryConvertFromJson( } // Dependencies - // TODO 3.0.0-beta21, a little complicated + // TODO vNext, a little complicated // IsPrerelease + // NuGet.org repository's response does contain 'isPrerelease' element so it can be accquired and set here. if (rootDom.TryGetProperty("isPrerelease", out JsonElement isPrereleaseElement)) { metadata["IsPrerelease"] = isPrereleaseElement.GetBoolean(); diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index eb262c4e0..72ad9dbef 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -803,13 +803,13 @@ private static XDocument LoadXDocument(string filePath) return XDocument.Load(xmlReader); } - private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) { - + private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) + { if (repoUri.AbsoluteUri.EndsWith("api/v2", StringComparison.OrdinalIgnoreCase)) { return PSRepositoryInfo.APIVersion.v2; } - else if (repoUri.AbsoluteUri.EndsWith("v3/index.json", StringComparison.OrdinalIgnoreCase)) + else if (repoUri.AbsoluteUri.EndsWith("/index.json", StringComparison.OrdinalIgnoreCase)) { return PSRepositoryInfo.APIVersion.v3; } diff --git a/src/code/ResponseUtilFactory.cs b/src/code/ResponseUtilFactory.cs index 175c8cb84..b42a396bd 100644 --- a/src/code/ResponseUtilFactory.cs +++ b/src/code/ResponseUtilFactory.cs @@ -21,6 +21,7 @@ public static ResponseUtil GetResponseUtil(PSRepositoryInfo repository) case PSRepositoryInfo.APIVersion.v3: currentResponseUtil = new V3ResponseUtil(repository); break; + case PSRepositoryInfo.APIVersion.local: currentResponseUtil = new LocalResponseUtil(repository); break; diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index e3103babe..38515b25d 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -21,17 +21,19 @@ internal class V3ServerAPICalls : ServerApiCall #region Members public override PSRepositoryInfo Repository { get; set; } private HttpClient _sessionClient { get; set; } + private bool _isNuGetRepo { get; set; } public FindResponseType v3FindResponseType = FindResponseType.ResponseString; private static readonly Hashtable[] emptyHashResponses = new Hashtable[]{}; + private static readonly string nugetRepoUri = "https://api.nuget.org/v3/index.json"; 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 itemsName = "items"; + private static readonly string countName = "count"; + private static readonly string versionName = "version"; 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"; + private static readonly string catalogEntryProperty = "catalogEntry"; + private static readonly string packageContentProperty = "packageContent"; #endregion @@ -48,6 +50,8 @@ public V3ServerAPICalls(PSRepositoryInfo repository, NetworkCredential networkCr }; _sessionClient = new HttpClient(handler); + + _isNuGetRepo = String.Equals(Repository.Uri.AbsoluteUri, nugetRepoUri, StringComparison.InvariantCultureIgnoreCase); } #endregion @@ -56,137 +60,41 @@ public V3ServerAPICalls(PSRepositoryInfo repository, NetworkCredential networkCr /// /// Find method which allows for searching for all packages from a repository and returns latest version for each. - /// Not supported + /// Not supported for V3 repository. /// public override FindResults FindAll(bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) { - string errMsg = $"Find all is not supported for the repository {Repository.Uri}"; + string errMsg = $"Find all is not supported for the V3 repository {Repository.Name}"; edi = ExceptionDispatchInfo.Capture(new InvalidOperationException(errMsg)); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } /// - /// 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 + /// Find method which allows for searching for packages with tag(s) from a repository and returns latest version for each. + /// This is supported only for the NuGet repository special case, not other V3 repositories. /// - public override FindResults FindTags(string[] tags, bool includePrerelease, ResourceType _type, out ExceptionDispatchInfo edi) + public override FindResults FindTags(string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) { - List responses = new List(); - string firstTag = tags[0]; // TODO: better err handle - - Hashtable resourceUrls = FindResourceType(new string[] { searchQueryServiceName, registrationsBaseUrlName }, out edi); - if (edi != null) + if (_isNuGetRepo) { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return FindTagsFromNuGetRepo(tags, includePrerelease, out edi); } - - string searchQueryServiceUrl = resourceUrls[searchQueryServiceName] as string; - string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; - - bool isNuGetRepo = searchQueryServiceUrl.Contains("nuget.org"); - - string query = isNuGetRepo ? $"{searchQueryServiceUrl}?q=tags:{firstTag.ToLower()}&prerelease={includePrerelease}&semVerLevel=2.0.0" : - $"{searchQueryServiceUrl}?q={firstTag.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) + else { + string errMsg = $"Find by Tags is not supported for the V3 repository {Repository.Name}"; + edi = ExceptionDispatchInfo.Capture(new InvalidOperationException(errMsg)); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - // determine if id matches our wildcard criteria - if (isNuGetRepo) - { - string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); - if (edi != null) - { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - string[] pkgTags = GetTagsFromJsonElement(tagsItem); - bool isTagMatch = true; - foreach (string rqTag in tags) - { - if (!pkgTags.Contains(rqTag, StringComparer.OrdinalIgnoreCase)) - { - isTagMatch = false; - break; - } - } - - if (isTagMatch) - { - string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); - if (edi != null) - { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - matchingResponses.Add(response); - } - } - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - } - } - - FindResults tagsFoundResult = new FindResults(stringResponse: matchingResponses.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - return tagsFoundResult; } /// - /// This functionality is not supported for V3 protocol server. /// Find method which allows for searching for packages with specified Command or DSCResource name. + /// Not supported for V3 repository. /// public override FindResults FindCommandOrDscResource(string[] tags, 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"; + string errMsg = $"Find by CommandName or DSCResource is not supported for the V3 server repository {Repository.Name}"; edi = ExceptionDispatchInfo.Capture(new InvalidOperationException(errMsg)); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); @@ -196,246 +104,172 @@ public override FindResults FindCommandOrDscResource(string[] tags, bool include /// 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) + /// We use the latest RegistrationBaseUrl version resource we can find and check if contains an entry with the package name. /// public override FindResults FindName(string packageName, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) { - Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName, registrationsBaseUrlName }, out edi); - if (edi != null) - { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - break; - } - } - } - - if (String.IsNullOrEmpty(response)) - { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindName() with {packageName} returned empty response.")); - } - - return new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return FindNameHelper(packageName, tags: Utils.EmptyStrArray, includePrerelease, type, out edi); } /// - /// Find method which allows for searching for single name and tag and returns latest version. + /// Find method which allows for searching for single name and specified tag(s) and returns latest version. /// Name: no wildcard support - /// Examples: Search "Newtonsoft.Json" - Tag "json" + /// Examples: Search "Newtonsoft.Json" -Tag "json" + /// We use the latest RegistrationBaseUrl version resource we can find and check if contains an entry with the package name. /// public override FindResults 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - break; - } - } - } - - if (String.IsNullOrEmpty(response)) - { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindNameWithTag() with {packageName} and tags {String.Join(",", tags)} returned empty response.")); - } - - return new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return FindNameHelper(packageName, tags, includePrerelease, type, out edi); } /// /// 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") + /// This is supported only for the NuGet repository special case, not other V3 repositories. /// public override FindResults 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - if (names.Length == 1) + if (_isNuGetRepo) { - // packageName: *get* -> q: get - // packageName: PowerShell* -> q: PowerShell - // packageName: *ShellGet -> q: ShellGet - querySearchTerm = names[0]; + return FindNameGlobbingFromNuGetRepo(packageName, tags: Utils.EmptyStrArray, includePrerelease, out edi); } else { - // *pow*get* - // pow*get -> only support this (V2) - // pow*get* - // *pow*get + string errMsg = $"Find with Name containing wildcards is not supported for the V3 server repository {Repository.Name}"; + edi = ExceptionDispatchInfo.Capture(new InvalidOperationException(errMsg)); - edi = ExceptionDispatchInfo.Capture(new ArgumentException("-Name with wildcards is only supported for scenarios similar to the following examples: PowerShell*, *ShellGet, *Shell*.")); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } + } - // 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) + /// + /// Find method which allows for searching for single name with wildcards and tag and returns latest version. + /// This is supported only for the NuGet repository special case, not other V3 repositories. + /// + public override FindResults FindNameGlobbingWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + if (_isNuGetRepo) { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return FindNameGlobbingFromNuGetRepo(packageName, tags, includePrerelease, out edi); } + else + { + string errMsg = $"Find with Name containing wildcards is not supported for the V3 server repository {Repository.Name}"; + edi = ExceptionDispatchInfo.Capture(new InvalidOperationException(errMsg)); - string searchQueryServiceUrl = resourceUrls[searchQueryServiceName] as string; - string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; - - string query = $"{searchQueryServiceUrl}?q={querySearchTerm}&prerelease={includePrerelease}&semVerLevel=2.0.0"; + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + } - // 2) call query with search term, get unique names, see which ones truly match - JsonElement[] matchingPkgIds = GetJsonElementArr(query, dataName, out edi); + /// + /// 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.*" + /// We use the latest RegistrationBaseUrl version resource we can find and check if contains an entry with the package name, then get all versions and match to satisfying versions. + /// + public override FindResults FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ExceptionDispatchInfo edi) + { + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out edi); if (edi != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - List matchingResponses = new List(); - foreach (var pkgId in matchingPkgIds) + List satisfyingVersions = new List(); + foreach (string response in versionedResponses) { - string id = string.Empty; - string latestVersion = string.Empty; - + JsonElement pkgVersionElement; try { - if (!pkgId.TryGetProperty(idName, out JsonElement idItem) || ! pkgId.TryGetProperty(versionName, out JsonElement versionItem)) + JsonDocument pkgVersionEntry = JsonDocument.Parse(response); + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(versionName, out pkgVersionElement)) { - string errMsg = $"FindNameGlobbing(): Name or Version element could not be found in response."; - edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain 'version' element.")); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - - 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; + edi = ExceptionDispatchInfo.Capture(e); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - // 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))) + if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion) && versionRange.Satisfies(pkgVersion)) { - string response = FindVersionHelper(registrationsBaseUrl, id, latestVersion, out edi); - - if (edi != null) + if (!pkgVersion.IsPrerelease || includePrerelease) { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + satisfyingVersions.Add(response); } - - matchingResponses.Add(response); } } - return new FindResults(stringResponse: matchingResponses.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return new FindResults(stringResponse: satisfyingVersions.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } /// - /// Find method which allows for searching for single name with wildcards and tag and returns latest version. - /// Name: supports wildcards - /// Examples: Search "Nuget.Server*" -Tag "nuget" + /// 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" + /// We use the latest RegistrationBaseUrl version resource we can find and check if contains an entry with the package name, then match to the specified version. /// - public override FindResults FindNameGlobbingWithTag(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + public override FindResults FindVersion(string packageName, string version, ResourceType type, out ExceptionDispatchInfo edi) + { + return FindVersionHelper(packageName, version, tags: Utils.EmptyStrArray, type, out edi); + } + + /// + /// Find method which allows for searching for single name with specific version and tag(s). + /// Name: no wildcard support + /// Version: no wildcard support + /// Examples: Search "NuGet.Server.Core" "3.0.0-beta" -Tag "core" + /// We use the latest RegistrationBaseUrl version resource we can find and check if contains an entry with the package name, then match to the specified version. + /// + public override FindResults FindVersionWithTag(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi) + { + return FindVersionHelper(packageName, version, tags: tags, type, out edi); + } + + /** INSTALL APIS **/ + + /// + /// Installs specific package. + /// Name: no wildcard support. + /// Examples: Install "Newtonsoft.json" + /// + public override Stream InstallName(string packageName, bool includePrerelease, out ExceptionDispatchInfo edi) + { + return InstallHelper(packageName, version: null, out edi); + } + + /// + /// Installs package with specific name and version. + /// Name: no wildcard support. + /// Version: no wildcard support. + /// Examples: Install "Newtonsoft.json" -Version "1.0.0.0" + /// Install "Newtonsoft.json" -Version "2.5.0-beta" + /// + public override Stream InstallVersion(string packageName, string version, out ExceptionDispatchInfo edi) + { + if (!NuGetVersion.TryParse(version, out NuGetVersion requiredVersion)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Version {version} to be installed is not a valid NuGet version.")); + return null; + } + + return InstallHelper(packageName, requiredVersion, out edi); + } + + #endregion + + #region Private Methods + + /// + /// Helper method called by FindNameGlobbing() and FindNameGlobbingWithTag() for special case where repository is NuGet.org repository. + /// + private FindResults FindNameGlobbingFromNuGetRepo(string packageName, string[] tags, bool includePrerelease, out ExceptionDispatchInfo edi) { var names = packageName.Split(new char[] { '*' }, StringSplitOptions.RemoveEmptyEntries); string querySearchTerm; @@ -463,28 +297,14 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[] return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - // 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - - 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); + JsonElement[] matchingPkgEntries = GetVersionedPackageEntriesFromSearchQueryResource(querySearchTerm, includePrerelease, out edi); if (edi != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } List matchingResponses = new List(); - foreach (var pkgId in matchingPkgIds) + foreach (var pkgEntry in matchingPkgEntries) { string id = string.Empty; string latestVersion = string.Empty; @@ -492,14 +312,14 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[] try { - if (!pkgId.TryGetProperty(idName, out JsonElement idItem) || !pkgId.TryGetProperty(versionName, out JsonElement versionItem)) + if (!pkgEntry.TryGetProperty(idName, out JsonElement idItem)) { - string errMsg = $"FindNameGlobbing(): Name or Version element could not be found in response."; + string errMsg = $"FindNameGlobbing(): Name element could not be found in response."; edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - if (!pkgId.TryGetProperty(tagsName, out JsonElement tagsItem)) + if (!pkgEntry.TryGetProperty(tagsName, out JsonElement tagsItem)) { string errMsg = $"FindNameGlobbing(): Tags element could not be found in response."; edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); @@ -507,525 +327,644 @@ public override FindResults FindNameGlobbingWithTag(string packageName, string[] } id = idItem.ToString(); - latestVersion = versionItem.ToString(); - pkgTags = GetTagsFromJsonElement(tagsElement: tagsItem); + // 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 = IsRequiredTagSatisfied(tagsItem, tags, out edi); + if (!isTagMatch) + { + continue; + } + + matchingResponses.Add(pkgEntry.ToString()); + } } + catch (Exception e) { - string errMsg = $"FindNameGlobbingWithTag(): Name or Version or Tags element could not be parsed from response due to exception {e.Message}."; + 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))) - { - 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 new FindResults(stringResponse: matchingResponses.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } /// - /// 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 FindResults FindVersionGlobbing(string packageName, VersionRange versionRange, bool includePrerelease, ResourceType type, bool getOnlyLatest, out ExceptionDispatchInfo edi) + /// Helper method called by FindTags() for special case where repository is NuGet.org repository. + /// + private FindResults FindTagsFromNuGetRepo(string[] tags, bool includePrerelease, out ExceptionDispatchInfo edi) { - Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName, registrationsBaseUrlName }, out edi); + string tagsQueryTerm = $"tags:{String.Join(" ", tags)}"; + // Get responses for all packages that contain the required tags + JsonElement[] tagPkgEntries = GetVersionedPackageEntriesFromSearchQueryResource(tagsQueryTerm, includePrerelease, out edi); if (edi != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; - string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + List matchingPkgResponses = new List(); + foreach (var pkgEntry in tagPkgEntries) + { + matchingPkgResponses.Add(pkgEntry.ToString()); + } + + return new FindResults(stringResponse: matchingPkgResponses.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } - bool isNuGetRepo = packageBaseAddressUrl.Contains("v3-flatcontainer"); - JsonElement[] pkgVersionsArr = GetPackageVersions(packageBaseAddressUrl, packageName, isNuGetRepo, out edi); + /// + /// Helper method called by FindName() and FindNameWithTag() + /// + private FindResults FindNameHelper(string packageName, string[] tags, bool includePrerelease, ResourceType type, out ExceptionDispatchInfo edi) + { + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out edi); if (edi != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - 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 new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } + string latestVersionResponse = String.Empty; + bool isTagMatch = true; + foreach (string response in versionedResponses) + { + try + { + JsonDocument pkgVersionEntry = JsonDocument.Parse(response); + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element for search with Name {packageName} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + if (!rootDom.TryGetProperty(tagsName, out JsonElement tagsItem)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{tagsName}' element for search with Name {packageName} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } - responses.Add(response); + if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) + { + if (!pkgVersion.IsPrerelease || includePrerelease) + { + // Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 so grabbing the first match suffices + latestVersionResponse = response; + isTagMatch = IsRequiredTagSatisfied(tagsItem, tags, out edi); + break; + } } } + catch (Exception e) + { + edi = ExceptionDispatchInfo.Capture(e); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } } - return new FindResults(stringResponse: responses.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } + if (String.IsNullOrEmpty(latestVersionResponse)) + { + string errMsg = $"FindName(): Package with Name {packageName} was not found in repository {Repository.Name}."; + edi = ExceptionDispatchInfo.Capture(new SpecifiedTagsNotFoundException(errMsg)); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + // Check and write error for tags matching requirement. If no tags were required the isTagMatch variable will be true. + if (!isTagMatch) + { + if (edi == null) + { + string errMsg = $"FindName(): Package with Name {packageName} and Tags {String.Join(", ", tags)} was not found in repository {Repository.Name}."; + edi = ExceptionDispatchInfo.Capture(new SpecifiedTagsNotFoundException(errMsg)); + } + + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + + return new FindResults(stringResponse: new string[] { latestVersionResponse }, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + /// - /// 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 - /// + /// Helper method called by FindVersion() and FindVersionWithTag() /// - public override FindResults FindVersion(string packageName, string version, ResourceType type, out ExceptionDispatchInfo edi) + private FindResults FindVersionHelper(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi) { - Hashtable resourceUrls = FindResourceType(new string[] { registrationsBaseUrlName }, out edi); + if (!NuGetVersion.TryParse(version, out NuGetVersion requiredVersion)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Version {version} to be found is not a valid NuGet version.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, catalogEntryProperty, isSearch: true, out edi); if (edi != null) { return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; + string latestVersionResponse = String.Empty; + bool isTagMatch = true; + foreach (string response in versionedResponses) + { + // Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 + try + { + JsonDocument pkgVersionEntry = JsonDocument.Parse(response); + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element for search with Name {packageName} and Version {version} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + if (!rootDom.TryGetProperty(tagsName, out JsonElement tagsItem)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{tagsName}' element for search with Name {packageName} and Version {version} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) + { + if (pkgVersion == requiredVersion) + { + latestVersionResponse = response; + isTagMatch = IsRequiredTagSatisfied(tagsItem, tags, out edi); + break; + } + } + } + catch (Exception e) + { + edi = ExceptionDispatchInfo.Capture(e); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + } - string response = FindVersionHelper(registrationsBaseUrl, packageName, version, out edi); - if (edi != null) + if (String.IsNullOrEmpty(latestVersionResponse)) { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindVersion(): Package with Name {packageName}, Version {version} was not found in repository {Repository.Name}")); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - if (String.IsNullOrEmpty(response)) + if (!isTagMatch) { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindVersion() with {packageName} and version {version} returned empty response.")); + if (edi == null) + { + string errMsg = $"FindVersion(): Package with Name {packageName}, Version {version} and Tags {String.Join(", ", tags)} was not found in repository {Repository.Name}."; + edi = ExceptionDispatchInfo.Capture(new SpecifiedTagsNotFoundException(errMsg)); + } + + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - return new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return new FindResults(stringResponse: new string[] { latestVersionResponse }, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } /// - /// Find method which allows for searching for single name with specific version and tag. - /// Name: no wildcard support - /// Version: no wildcard support - /// Examples: Search "NuGet.Server.Core" -Version "3.0.0-beta" -Tag "nuget" - /// 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 FindResults FindVersionWithTag(string packageName, string version, string[] tags, ResourceType type, out ExceptionDispatchInfo edi) + /// Helper method that is called by InstallName() and InstallVersion() + /// For InstallName() we want latest version installed (so version parameter passed in will be null), for InstallVersion() we want specified, non-null version installed. + /// + private Stream InstallHelper(string packageName, NuGetVersion version, out ExceptionDispatchInfo edi) { - Hashtable resourceUrls = FindResourceType(new string[] { registrationsBaseUrlName }, out edi); - if (edi != null) + Stream pkgStream = null; + bool getLatestVersion = true; + if (version != null) { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + getLatestVersion = false; } - string registrationsBaseUrl = resourceUrls[registrationsBaseUrlName] as string; - - string response = FindVersionHelper(registrationsBaseUrl, packageName, version, out edi); + string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, packageContentProperty, isSearch: false, out edi); if (edi != null) { - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + return pkgStream; } - bool isTagMatch = DetermineTagsPresent(response: response, tags: tags, out edi); + if (versionedResponses.Length == 0) + { + string errorMsg = $"Package with Name {packageName} and Version {version} could not be found in repository {Repository.Name}"; + edi = ExceptionDispatchInfo.Capture(new Exception(errorMsg)); + return null; + } - if (!isTagMatch) + string pkgContentUrl = String.Empty; + if (getLatestVersion) { - if (edi == null) + pkgContentUrl = versionedResponses[0]; + } + else + { + // loop through responses to find one containing required version + foreach (string response in versionedResponses) { - string errMsg = $"FindVersionWithTag(): Tags required were not found in package {packageName} {version.ToString()}."; - edi = ExceptionDispatchInfo.Capture(new SpecifiedTagsNotFoundException(errMsg)); + // Response will be "packageContent" element value that looks like: "{packageBaseAddress}/{packageName}/{normalizedVersion}/{packageName}.{normalizedVersion}.nupkg" + // Ex: https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg + if (response.Contains(version.ToNormalizedString())) + { + pkgContentUrl = response; + break; + } } + } - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + if (String.IsNullOrEmpty(pkgContentUrl)) + { + string errorMsg = $"Package with Name {packageName} and Version {version} could not be found in repository {Repository.Name}"; + edi = ExceptionDispatchInfo.Capture(new Exception(errorMsg)); + return null; } - if (String.IsNullOrEmpty(response)) + var content = HttpRequestCallForContent(pkgContentUrl, out edi); + if (edi != null) { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"FindVersion() with {packageName}, tags {String.Join(", ", tags)} and version {version} returned empty response.")); + return null; } - return new FindResults(stringResponse: new string[] { response }, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + pkgStream = content.ReadAsStreamAsync().Result; + return pkgStream; } - - /** 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 + /// Gets the versioned package entries from the RegistrationsBaseUrl resource + /// i.e when the package Name being searched for does not contain wildcard + /// This is called by FindNameHelper(), FindVersionHelper(), FindVersionGlobbing(), InstallHelper() /// - public override Stream InstallName(string packageName, bool includePrerelease, out ExceptionDispatchInfo edi) + private string[] GetVersionedPackageEntriesFromRegistrationsResource(string packageName, string propertyName, bool isSearch, out ExceptionDispatchInfo edi) { - Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName }, out edi); + string[] responses = Utils.EmptyStrArray; + Dictionary resources = GetResourcesFromServiceIndex(out edi); if (edi != null) { - return null; + return responses; } - string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; - - bool isNuGetRepo = packageBaseAddressUrl.Contains("v3-flatcontainer"); - - JsonElement[] pkgVersionsArr = GetPackageVersions(packageBaseAddressUrl, packageName, isNuGetRepo, out edi); + string registrationsBaseUrl = FindRegistrationsBaseUrl(resources, out edi); if (edi != null) { - return null; + return responses; } - foreach (JsonElement version in pkgVersionsArr) + responses = GetVersionedResponsesFromRegistrationsResource(registrationsBaseUrl, packageName, propertyName, isSearch, out edi); + if (edi != null) { - 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 responseStream = InstallVersion(packageName, version.ToString(), out edi); - if (edi != null) - { - return null; - } - - return responseStream; - } - } + return Utils.EmptyStrArray; } - return null; + return responses; } /// - /// 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 Stream InstallVersion(string packageName, string version, out ExceptionDispatchInfo edi) + /// Gets the versioned package entries from SearchQueryService resource + /// i.e when the package Name being searched for contains wildcards or a Tag query search is performed + /// This is called by FindNameGlobbingFromNuGetRepo() and FindTagsFromNuGetRepo() + /// + private JsonElement[] GetVersionedPackageEntriesFromSearchQueryResource(string queryTerm, bool includePrerelease, out ExceptionDispatchInfo edi) { - Hashtable resourceUrls = FindResourceType(new string[] { packageBaseAddressName }, out edi); + JsonElement[] pkgEntries = new JsonElement[]{}; + Dictionary resources = GetResourcesFromServiceIndex(out edi); if (edi != null) { - return null; + return pkgEntries; } - string packageBaseAddressUrl = resourceUrls[packageBaseAddressName] as string; + string searchQueryServiceUrl = FindSearchQueryService(resources, out edi); + if (edi != null) + { + return pkgEntries; + } - string pkgName = packageName.ToLower(); - string installPkgUrl = $"{packageBaseAddressUrl}{pkgName}/{version}/{pkgName}.{version}.nupkg"; + string query = $"{searchQueryServiceUrl}?q={queryTerm}&prerelease={includePrerelease}&semVerLevel=2.0.0"; - var content = HttpRequestCallForContent(installPkgUrl, out edi); + // Get responses for all packages that contain the required tags + pkgEntries = GetJsonElementArr(query, dataName, out edi); if (edi != null) { - return null; + return new JsonElement[]{}; } - return content.ReadAsStreamAsync().Result; + return pkgEntries; } - #endregion + /// + /// Finds all resources present in the repository's service index. + /// For example: https://api.nuget.org/v3/index.json + /// + private Dictionary GetResourcesFromServiceIndex(out ExceptionDispatchInfo edi) + { + Dictionary resources = new Dictionary(); + JsonElement[] resourcesArray = GetJsonElementArr($"{Repository.Uri}", resourcesName, out edi); + if (edi != null) + { + return resources; + } - #region Private Methods + foreach (JsonElement resource in resourcesArray) + { + try + { + if (!resource.TryGetProperty("@type", out JsonElement typeElement)) + { + edi = ExceptionDispatchInfo.Capture(new JsonParsingException($"@type element not found for resource in service index for repository {Repository.Name}")); + return new Dictionary(); + } + + if (!resource.TryGetProperty("@id", out JsonElement idElement)) + { + edi = ExceptionDispatchInfo.Capture(new JsonParsingException($"@id element not found for resource in service index for repository {Repository.Name}")); + return new Dictionary(); + } + + if (!resources.ContainsKey(typeElement.ToString())) + { + // Some resources have a primary and secondary entry. The @id value is the same, so we only choose the primary entry. + resources.Add(typeElement.ToString(), idElement.ToString()); + } + } + catch (Exception e) + { + string errMsg = $"Exception parsing service index JSON for respository {Repository.Name} with error: {e.Message}"; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); + return new Dictionary(); + } + } + return resources; + } + /// - /// Helper method that makes the HTTP request for the V3 server protocol url passed in for find APIs. + /// Gets the resource of type "RegistrationBaseUrl" from the repository's resources. + /// A repository can have multiple resources of type "RegistrationsBaseUrl" so it finds the best match according to the guideline comment in the method. /// - private String HttpRequestCall(string requestUrlV3, out ExceptionDispatchInfo edi) + private string FindRegistrationsBaseUrl(Dictionary resources, out ExceptionDispatchInfo edi) { edi = null; - string response = string.Empty; + string registrationsBaseUrl = String.Empty; - try - { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV3); + /** + If RegistrationsBaseUrl/3.6.0 exists, use RegistrationsBaseUrl/3.6.0 + Otherwise, if RegistrationsBaseUrl/3.4.0 exists, use RegistrationsBaseUrl/3.4.0 + Otherwise, if RegistrationsBaseUrl/3.0.0-rc exists, use RegistrationsBaseUrl/3.0.0-rc + Otherwise, if RegistrationsBaseUrl/3.0.0-beta exists, use RegistrationsBaseUrl/3.0.0-beta + Otherwise, if RegistrationsBaseUrl exists, use RegistrationsBaseUrl + Otherwise, report an error + */ - response = SendV3RequestAsync(request, _sessionClient).GetAwaiter().GetResult(); + if (resources.ContainsKey("RegistrationsBaseUrl/3.6.0")) + { + registrationsBaseUrl = resources["RegistrationsBaseUrl/3.6.0"]; } - catch (HttpRequestException e) + else if (resources.ContainsKey("RegistrationsBaseUrl/3.4.0")) { - edi = ExceptionDispatchInfo.Capture(e); + registrationsBaseUrl = resources["RegistrationsBaseUrl/3.4.0"]; } - catch (ArgumentNullException e) + else if (resources.ContainsKey("RegistrationsBaseUrl/3.0.0-rc")) { - edi = ExceptionDispatchInfo.Capture(e); + registrationsBaseUrl = resources["RegistrationsBaseUrl/3.0.0-rc"]; } - catch (InvalidOperationException e) + else if (resources.ContainsKey("RegistrationsBaseUrl/3.0.0-beta")) { - edi = ExceptionDispatchInfo.Capture(e); + registrationsBaseUrl = resources["RegistrationsBaseUrl/3.0.0-beta"]; } - catch (Exception e) + else if (resources.ContainsKey("RegistrationsBaseUrl")) { - edi = ExceptionDispatchInfo.Capture(e); + registrationsBaseUrl = resources["RegistrationsBaseUrl"]; + } + else + { + edi = ExceptionDispatchInfo.Capture(new V3ResourceNotFoundException($"RegistrationBaseUrl resource could not be found for Repository '{Repository.Name}'")); } - return response; + return registrationsBaseUrl; } /// - /// Helper method that makes the HTTP request for the V3 server protocol url passed in for install APIs. + /// Gets the resource of type "SearchQueryService" from the repository's resources. + /// A repository can have multiple resources of type "SearchQueryService" so it finds the best match according to the guideline comment in the method. /// - private HttpContent HttpRequestCallForContent(string requestUrlV3, out ExceptionDispatchInfo edi) + private string FindSearchQueryService(Dictionary resources, out ExceptionDispatchInfo edi) { edi = null; - HttpContent content = null; + string searchQueryServiceUrl = String.Empty; - try + if (resources.ContainsKey("SearchQueryService/3.5.0")) { - HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV3); - - content = SendV3RequestForContentAsync(request, _sessionClient).GetAwaiter().GetResult(); + searchQueryServiceUrl = resources["SearchQueryService/3.5.0"]; } - catch (HttpRequestException e) + else if (resources.ContainsKey("SearchQueryService/3.0.0-rc")) { - edi = ExceptionDispatchInfo.Capture(e); + searchQueryServiceUrl = resources["SearchQueryService/3.0.0-rc"]; } - catch (ArgumentNullException e) + else if (resources.ContainsKey("SearchQueryService/3.0.0-beta")) { - edi = ExceptionDispatchInfo.Capture(e); + searchQueryServiceUrl = resources["SearchQueryService/3.0.0-beta"]; } - catch (InvalidOperationException e) + else if (resources.ContainsKey("SearchQueryService")) { - edi = ExceptionDispatchInfo.Capture(e); + searchQueryServiceUrl = resources["SearchQueryService"]; + } + else + { + edi = ExceptionDispatchInfo.Capture(new V3ResourceNotFoundException($"SearchQueryService resource could not be found for Repository '{Repository.Name}'")); } - return content; + return searchQueryServiceUrl; } /// - /// Helper method that makes finds the specified V3 server protocol resources from the service index. - /// - private Hashtable FindResourceType(string[] resourceTypeName, out ExceptionDispatchInfo edi) + /// Helper method iterates through the entries in the registrationsUrl for a specific package and all its versions. + /// This contains an inner items element (containing the package metadata) and the packageContent element (containing URI through which the .nupkg can be downloaded) + /// This can be the "catalogEntry" or "packageContent" property. + /// The "catalogEntry" property is used for search, and the value is package metadata. + /// The "packageContent" property is used for download, and the value is a URI for the .nupkg file. + /// + /// + private string[] GetVersionedResponsesFromRegistrationsResource(string registrationsBaseUrl, string packageName, string property, bool isSearch, out ExceptionDispatchInfo edi) { - Hashtable resourceHash = new Hashtable(); - JsonElement[] resources = GetJsonElementArr($"{Repository.Uri}", resourcesName, out edi); + List versionedResponses = new List(); + string[] versionedResponseArr; + var requestPkgMapping = registrationsBaseUrl.EndsWith("/") ? $"{registrationsBaseUrl}{packageName.ToLower()}/index.json" : $"{registrationsBaseUrl}/{packageName.ToLower()}/index.json"; + + string pkgMappingResponse = HttpRequestCall(requestPkgMapping, out edi); if (edi != null) { - return resourceHash; + return Utils.EmptyStrArray; } - foreach (JsonElement resource in resources) + try { - try + // parse out JSON response we get from RegistrationsUrl + JsonDocument pkgVersionEntry = JsonDocument.Parse(pkgMappingResponse); + + // The response has a "items" array element, which only has useful 1st element + JsonElement rootDom = pkgVersionEntry.RootElement; + rootDom.TryGetProperty(itemsName, out JsonElement itemsElement); + if (itemsElement.GetArrayLength() == 0) { - 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; - } - } - } + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain '{itemsName}' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; } - catch (Exception e) + + JsonElement firstItem = itemsElement[0]; + + // https://api.nuget.org/v3/registration5-gz-semver2/test_module/index.json + // The "items" property contains an inner "items" element and a "count" element + // The inner "items" property is the metadata array for each version of the package. + // The "count" property represents how many versions are present for that package, (i.e how many elements are in the inner "items" array) + + if (!firstItem.TryGetProperty(itemsName, out JsonElement innerItemsElements)) { - string errMsg = $"Exception parsing JSON for respository {Repository.Uri} with error: {e.Message}"; - edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); - return resourceHash; + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{itemsName}' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; + } + + if (!firstItem.TryGetProperty(countName, out JsonElement countElement) || !countElement.TryGetInt32(out int count)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{countName}' element or it is not a valid integer, for package with Name {packageName}.")); + return Utils.EmptyStrArray; } - if (resourceHash.Count == resourceTypeName.Length) + if (!firstItem.TryGetProperty("upper", out JsonElement upperVersionElement)) { - break; + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner 'upper' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; } - } - foreach (string resourceType in resourceTypeName) - { - if (!resourceHash.ContainsKey(resourceType)) + for (int i = 0; i < count; i++) { - string errMsg = $"FindResourceType(): Could not find resource type {resourceType} from the service index."; - edi = ExceptionDispatchInfo.Capture(new V3ResourceNotFoundException(errMsg)); - break; + // Get the specific entry for each package version + JsonElement versionedItem = innerItemsElements[i]; + + // For search: + // The "catalogEntry" property in the specific package version entry contains package metadata + // For download: + // The "packageContent" property in the specific package version entry has the .nupkg URI for each version of the package. + if (!versionedItem.TryGetProperty(property, out JsonElement metadataElement)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{property}' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; + } + + versionedResponses.Add(metadataElement.ToString()); + } + + // Reverse array of versioned responses, if needed, so that version entries are in descending order. + string upperVersion = upperVersionElement.ToString(); + versionedResponseArr = versionedResponses.ToArray(); + if (isSearch) + { + if (!IsLatestVersionFirstForSearch(versionedResponseArr, upperVersion, out edi)) + { + Array.Reverse(versionedResponseArr); + } } + else + { + if (!IsLatestVersionFirstForInstall(versionedResponseArr, upperVersion, out edi)) + { + Array.Reverse(versionedResponseArr); + } + } + } + catch (Exception e) + { + edi = ExceptionDispatchInfo.Capture(e); + return Utils.EmptyStrArray; } - return resourceHash; + return versionedResponseArr; } /// - /// Helper method finds package with name and specified version - /// - private string FindVersionHelper(string registrationsBaseUrl, string packageName, string version, out ExceptionDispatchInfo edi) + /// Returns true if the metadata entries are arranged in descending order with respect to the package's version. + /// ADO feeds usually return version entries in descending order, but Nuget.org repository returns them in ascending order. + /// + private bool IsLatestVersionFirstForSearch(string[] versionedResponses, string upperVersion, 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) + edi = null; + bool latestVersionFirst = true; + + // We don't need to perform this check if no responses, or single response + if (versionedResponses.Length < 2) { - return String.Empty; + return latestVersionFirst; } - string catalogEntryUrl = string.Empty; - try + string firstResponse = versionedResponses[0]; + JsonDocument firstResponseJson = JsonDocument.Parse(firstResponse); + JsonElement firstResponseDom = firstResponseJson.RootElement; + if (!firstResponseDom.TryGetProperty(versionName, out JsonElement firstVersionElement)) { - JsonDocument pkgMappingDom = JsonDocument.Parse(pkgMappingResponse); - JsonElement rootPkgMappingDom = pkgMappingDom.RootElement; + edi = ExceptionDispatchInfo.Capture(new JsonParsingException($"Response did not contain '{versionName}' element")); + return latestVersionFirst; + } - if (!rootPkgMappingDom.TryGetProperty("catalogEntry", out JsonElement catalogEntryUrlElement)) + string firstVersion = firstVersionElement.ToString(); + if (NuGetVersion.TryParse(upperVersion, out NuGetVersion upperPkgVersion) && NuGetVersion.TryParse(firstVersion, out NuGetVersion firstPkgVersion)) + { + if (firstPkgVersion != upperPkgVersion) { - string errMsg = $"FindVersionHelper(): CatalogEntry element could not be found in response or was empty."; - edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); - return String.Empty; + latestVersionFirst = false; } - - catalogEntryUrl = catalogEntryUrlElement.ToString(); } - catch (Exception e) + + return latestVersionFirst; + } + + /// + /// Returns true if the nupkg URI entries for each package version are arranged in descending order with respect to the package's version. + /// ADO feeds usually return version entries in descending order, but Nuget.org repository returns them in ascending order. + /// + private bool IsLatestVersionFirstForInstall(string[] versionedResponses, string upperVersion, out ExceptionDispatchInfo edi) + { + edi = null; + bool latestVersionFirst = true; + + // We don't need to perform this check if no responses, or single response + if (versionedResponses.Length < 2) { - string errMsg = $"FindVersionHelper(): Exception parsing JSON for respository {Repository.Uri} with error: {e.Message}"; - edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); - return String.Empty; + return latestVersionFirst; } - string response = HttpRequestCall(catalogEntryUrl, out edi); - if (edi != null) + string firstResponse = versionedResponses[0]; + // for Install, response will be a URI value for the package .nupkg, not JSON + if (!firstResponse.Contains(upperVersion)) { - return String.Empty; + latestVersionFirst = false; } - return response; + return latestVersionFirst; } /// - /// Helper method that determines if specified tags are present in response representing package(s). + /// Helper method that determines if specified tags are present in package's tags. /// - private bool DetermineTagsPresent(string response, string[] tags, out ExceptionDispatchInfo edi) + private bool IsRequiredTagSatisfied(JsonElement tagsElement, string[] tags, out ExceptionDispatchInfo edi) { edi = null; string[] pkgTags = Utils.EmptyStrArray; + // Get the package's tags from the tags JsonElement try { - JsonDocument pkgMappingDom = JsonDocument.Parse(response); - JsonElement rootPkgMappingDom = pkgMappingDom.RootElement; - - if (!rootPkgMappingDom.TryGetProperty(tagsName, out JsonElement tagsElement)) + List tagsFound = new List(); + JsonElement[] pkgTagElements = tagsElement.EnumerateArray().ToArray(); + foreach (JsonElement tagItem in pkgTagElements) { - string errMsg = $"FindNameWithTag(): Tags element could not be found in response or was empty."; - edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); - return false; + tagsFound.Add(tagItem.ToString().ToLower()); } - pkgTags = GetTagsFromJsonElement(tagsElement: tagsElement); + pkgTags = tagsFound.ToArray(); } catch (Exception e) { - string errMsg = $"DetermineTagsPresent(): Exception parsing JSON for respository {Repository.Uri} with error: {e.Message}"; + string errMsg = $"DetermineTagsPresent(): Exception parsing 'Tags' element found in JSON from respository {Repository.Name} with error: {e.Message}"; edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); return false; } - bool isTagMatch = DeterminePkgTagsSatisfyRequiredTags(pkgTags: pkgTags, requiredTags: tags); - - return isTagMatch; - } - - /// - /// Helper method that finds all tags for package with the given tags JSonElement. - /// - 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(); - } - - /// - /// Helper method that compares the tags requests to be present to the tags present in the package. - /// - private bool DeterminePkgTagsSatisfyRequiredTags(string[] pkgTags, string[] requiredTags) - { + // determine if all required tags are present within package's tags. bool isTagMatch = true; - - foreach (string tag in requiredTags) + foreach (string requiredTag in tags) { - if (!pkgTags.Contains(tag, StringComparer.OrdinalIgnoreCase)) + if (!pkgTags.Contains(requiredTag, StringComparer.OrdinalIgnoreCase)) { isTagMatch = false; break; @@ -1035,27 +974,6 @@ private bool DeterminePkgTagsSatisfyRequiredTags(string[] pkgTags, string[] requ return isTagMatch; } - /// - /// Helper method that returns a flattened list of all versions present for a package. - /// Implementation note: NuGet server and Azure Artifacts server return this flattened version list in opposite orders so we reverse array accordingly. - /// - 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(); - } - /// /// Helper method that parses response for given property and returns result for that property as a JsonElement array. /// @@ -1085,10 +1003,74 @@ private JsonElement[] GetJsonElementArr(string request, string propertyName, out return pkgsArr; } + /// + /// Helper method that makes the HTTP request for the V3 server protocol url passed in for find APIs. + /// + 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, _sessionClient).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; + } + + /// + /// Helper method that makes the HTTP request for the V3 server protocol url passed in for install APIs. + /// + private HttpContent HttpRequestCallForContent(string requestUrlV3, out ExceptionDispatchInfo edi) + { + edi = null; + HttpContent content = null; + + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrlV3); + + content = SendV3RequestForContentAsync(request, _sessionClient).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; + } + /// /// Helper method called by HttpRequestCall() that makes the HTTP request for string response. /// - public static async Task SendV3RequestAsync(HttpRequestMessage message, HttpClient s_client) + private static async Task SendV3RequestAsync(HttpRequestMessage message, HttpClient s_client) { string errMsg = "SendV3RequestAsync(): Error occured while trying to retrieve response: "; @@ -1118,7 +1100,7 @@ public static async Task SendV3RequestAsync(HttpRequestMessage message, /// /// Helper method called by HttpRequestCallForContent() that makes the HTTP request for string response. /// - public static async Task SendV3RequestForContentAsync(HttpRequestMessage message, HttpClient s_client) + private static async Task SendV3RequestForContentAsync(HttpRequestMessage message, HttpClient s_client) { string errMsg = "SendV3RequestForContentAsync(): Error occured while trying to retrieve response for content: "; diff --git a/test/FindPSResourceTests/FindPSResourceADOServer.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceADOServer.Tests.ps1 new file mode 100644 index 000000000..68dee37d4 --- /dev/null +++ b/test/FindPSResourceTests/FindPSResourceADOServer.Tests.ps1 @@ -0,0 +1,213 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$modPath = "$psscriptroot/../PSGetTestUtils.psm1" +Import-Module $modPath -Force -Verbose + +Describe 'Test HTTP Find-PSResource for ADO Server Protocol' -tags 'CI' { + + BeforeAll{ + $testModuleName = "test_local_mod" + $ADORepoName = "PSGetTestingPublicFeed" + $ADORepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell-public-test/nuget/v3/index.json" + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + It "find resource given specific Name, Version null" { + # FindName() + $res = Find-PSResource -Name $testModuleName -Repository $ADORepoName + $res.Name | Should -Be $testModuleName + $res.Version | Should -Be "5.0.0" + } + + It "should not find resource given nonexistant Name" { + # FindName() + $res = Find-PSResource -Name NonExistantModule -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $res | Should -BeNullOrEmpty + } + + It "find resource(s) given wildcard Name" { + # FindNameGlobbing + $wildcardName = "test_local_m*" + $res = Find-PSResource -Name $wildcardName -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameGlobbingFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + $res | Should -BeNullOrEmpty + } + + $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 $ADORepoName + $res | Should -Not -BeNullOrEmpty + 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 $ADORepoName + $res | Should -Not -BeNullOrEmpty + $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_local_mod resource's latest version is a prerelease version, before that it has a non-prerelease version + $res = Find-PSResource -Name $testModuleName -Repository $ADORepoName + $res.Version | Should -Be "5.0.0" + + $resPrerelease = Find-PSResource -Name $testModuleName -Prerelease -Repository $ADORepoName + $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 $ADORepoName + $resWithPrerelease = Find-PSResource -Name $testModuleName -Version "*" -Repository $ADORepoName + $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 $ADORepoName + $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_local_mod package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTag -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameFail,Microsoft.PowerShell.PSResourceGet.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 $ADORepoName + $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_local_mod package + $res = Find-PSResource -Name $testModuleName -Tag $requiredTags -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } + + It "should not find resources when given Name with wildcard and Tag proprties" { + # FindNameGlobbingWithTag() + $requiredTag = "test" + $nameWithWildcard = "test_local_m*" + $res = Find-PSResource -Name $nameWithWildcard -Tag $requiredTag -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindNameGlobbingFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } + + 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 $ADORepoName + $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_local_mod package + $res = Find-PSResource -Name $testModuleName -Version "5.0.0.0" -Tag $requiredTag -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindVersionFail,Microsoft.PowerShell.PSResourceGet.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 $ADORepoName + $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 $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindVersionFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } + + It "should not find resources given Tag property" { + # FindTag() + $tagToFind = "Tag2" + $res = Find-PSResource -Tag $tagToFind -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindTagFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } + + It "should not find resource given CommandName" { + # FindCommandOrDSCResource() + $res = Find-PSResource -CommandName "command" -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDSCResourceFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } + + It "should not find resource given DscResourceName" { + # FindCommandOrDSCResource() + $res = Find-PSResource -DscResourceName "dscResource" -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindCommandOrDSCResourceFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } + + It "should not find all resources given Name '*'" { + # FindAll() + $res = Find-PSResource -Name "*" -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $res | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "FindAllFail,Microsoft.PowerShell.PSResourceGet.Cmdlets.FindPSResource" + } +} diff --git a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 new file mode 100644 index 000000000..8cf65b81f --- /dev/null +++ b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 @@ -0,0 +1,264 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ProgressPreference = "SilentlyContinue" +$modPath = "$psscriptroot/../PSGetTestUtils.psm1" +Import-Module $modPath -Force -Verbose + +Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { + + BeforeAll { + $testModuleName = "test_local_mod" + $testModuleName2 = "test_local_mod2" + $testScriptName = "test_ado_script" + $ADORepoName = "PSGetTestingPublicFeed" + $ADORepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/powershell-public-test/nuget/v3/index.json" + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri + } + + AfterEach { + Uninstall-PSResource $testModuleName, $testModuleName2, $testScriptName -Version "*" -SkipDependencyCheck -ErrorAction SilentlyContinue + } + + AfterAll { + Get-RevertPSResourceRepositoryFile + } + + $testCases = @{Name="*"; ErrorId="NameContainsWildcard"}, + @{Name="Test_local_m*"; ErrorId="NameContainsWildcard"}, + @{Name="Test?local","Test[local"; ErrorId="ErrorFilteringNamesForUnsupportedWildcards"} + + It "Should not install resource with wildcard in name" -TestCases $testCases { + param($Name, $ErrorId) + Install-PSResource -Name $Name -Repository $ADORepoName -ErrorVariable err -ErrorAction SilentlyContinue + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + $res = Get-InstalledPSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Install specific module resource by name" { + Install-PSResource -Name $testModuleName -Repository $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $testScriptName + $pkg.Name | Should -Be $testScriptName + $pkg.Version | Should -Be "1.0.0" + } + + It "Install multiple resources by name" { + $pkgNames = @($testModuleName, $testModuleName2) + Install-PSResource -Name $pkgNames -Repository $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $pkgNames + $pkg.Name | Should -Be $pkgNames + } + + It "Should not install resource given nonexistant name" { + Install-PSResource -Name "NonExistantModule" -Repository $ADORepoName -TrustRepository -ErrorVariable err -ErrorAction SilentlyContinue + $pkg = Get-InstalledPSResource "NonExistantModule" + $pkg | Should -BeNullOrEmpty + $err.Count | Should -BeGreaterThan 0 + $err[0].FullyQualifiedErrorId | Should -BeExactly "InstallPackageFailure,Microsoft.PowerShell.PSResourceGet.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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository -ErrorAction SilentlyContinue + } + catch + {} + $Error[0].FullyQualifiedErrorId | Should -be "IncorrectVersionFormat,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + + $res = Get-InstalledPSResource $testModuleName + $res | Should -BeNullOrEmpty + } + + It "Install resource when given Name, Version '*', should install the latest version" { + Install-PSResource -Name $testModuleName -Version "*" -Repository $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName | Install-PSResource -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + It "Install resource with companyname and repository source location and validate properties" { + Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + + $pkg.CompanyName | Should -Be "None" + $pkg.RepositorySourceLocation | Should -Be $ADORepoUri + } + + # Windows only + It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $ADORepoName -TrustRepository -Scope CurrentUser + $pkg = Get-InstalledPSResource $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 $testModuleName -Repository $ADORepoName -TrustRepository -Scope AllUsers -Verbose + $pkg = Get-InstalledPSResource $testModuleName -Scope AllUsers + $pkg.Name | Should -Be $testModuleName + $pkg.InstalledLocation.ToString().Contains("Program Files") | Should -Be $true + } + + # Windows only + It "Install resource under no specified scope - Windows only" -Skip:(!(Get-IsWindows)) { + Install-PSResource -Name $testModuleName -Repository $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository -Scope CurrentUser + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $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 $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + Install-PSResource -Name $testModuleName -Repository $ADORepoName -TrustRepository -WarningVariable WarningVar -warningaction SilentlyContinue + $WarningVar | Should -Not -BeNullOrEmpty + } + + It "Reinstall resource that is already installed with -Reinstall parameter" { + Install-PSResource -Name $testModuleName -Repository $ADORepoName -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + Install-PSResource -Name $testModuleName -Repository $ADORepoName -Reinstall -TrustRepository + $pkg = Get-InstalledPSResource $testModuleName + $pkg.Name | Should -Be $testModuleName + $pkg.Version | Should -Be "5.0.0" + } + + It "Install PSResourceInfo object piped in" { + Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $ADORepoName | Install-PSResource -TrustRepository + $res = Get-InstalledPSResource -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 $ADORepoName -PassThru -TrustRepository + $res.Name | Should -Contain $testModuleName + } +} + +Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { + + BeforeAll { + $testModuleName = "TestModule" + $testModuleName2 = "testModuleWithlicense" + Get-NewPSResourceRepositoryFile + Register-LocalRepos + } + + AfterEach { + Uninstall-PSResource $testModuleName, $testModuleName2 -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 $testModuleName -Repository $TestGalleryName -Scope AllUsers + $pkg = Get-Module $testModuleName -ListAvailable + $pkg.Name | Should -Be $testModuleName + $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 $testModuleName2 -Repository $TestGalleryName + $pkg = Get-InstalledPSResource $testModuleName2 + $pkg.Name | Should -Be $testModuleName2 + $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 $testModuleName -Repository $TestGalleryName -confirm:$false + + $pkg = Get-Module $testModuleName -ListAvailable + $pkg.Name | Should -Be $testModuleName + + Set-PSResourceRepository PoshTestGallery -Trusted + } +} diff --git a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 index d8e5731e1..07f266d6d 100644 --- a/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceLocal.Tests.ps1 @@ -194,9 +194,9 @@ Describe 'Test Install-PSResource for local repositories' -tags 'CI' { # Windows only It "Install resource under AllUsers scope - Windows only" -Skip:(!((Get-IsWindows) -and (Test-IsAdmin))) { Install-PSResource -Name $testModuleName -Repository $localRepo -TrustRepository -Scope AllUsers -Verbose - $pkg = Get-Module $testModuleName -ListAvailable + $pkg = Get-InstalledPSResource $testModuleName -Scope AllUsers $pkg.Name | Should -Be $testModuleName - $pkg.Path.ToString().Contains("Program Files") + $pkg.InstalledLocation.ToString().Contains("Program Files") | Should -Be $true } # Windows only diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 index 00ddd3285..c43b1d365 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -24,7 +24,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } AfterEach { - Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "testModuleWithlicense", "TestFindModule", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue + Uninstall-PSResource "test_module", "test_module2", "test_script", "TestModule99", "test_module_with_license", "TestFindModule", "PackageManagement" -SkipDependencyCheck -ErrorAction SilentlyContinue } AfterAll { @@ -40,6 +40,8 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { Install-PSResource -Name $Name -Repository $NuGetGalleryName -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -BeGreaterThan 0 $err[0].FullyQualifiedErrorId | Should -BeExactly "$ErrorId,Microsoft.PowerShell.PSResourceGet.Cmdlets.InstallPSResource" + $res = Get-InstalledPSResource $testModuleName + $res | Should -BeNullOrEmpty } It "Install specific module resource by name" { @@ -143,14 +145,15 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { ($env:PSModulePath).Contains($pkg.InstalledLocation) } - It "Install resource with companyname, copyright and repository source location and validate properties" { + It "Install resource with companyname and repository source location and validate properties" { Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository $NuGetGalleryName -TrustRepository $pkg = Get-InstalledPSResource $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." + # Broken now, tracked in issue + # $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." $pkg.RepositorySourceLocation | Should -Be $NuGetGalleryUri } @@ -241,12 +244,12 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { # (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-InstalledPSResource "testModuleWithlicense" - # $pkg.Name | Should -Be "testModuleWithlicense" - # $pkg.Version | Should -Be "0.0.3.0" - # } + It "Install resource that requires accept license with -AcceptLicense flag" { + Install-PSResource -Name "test_module_with_license" -Repository $NuGetGalleryName -AcceptLicense + $pkg = Get-InstalledPSResource "test_module_with_license" + $pkg.Name | Should -Be "test_module_with_license" + $pkg.Version | Should -Be "2.0.0" + } It "Install PSResourceInfo object piped in" { Find-PSResource -Name $testModuleName -Version "1.0.0.0" -Repository $NuGetGalleryName | Install-PSResource -TrustRepository @@ -367,7 +370,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidatio BeforeAll { $testModuleName = "TestModule" - $testModuleName2 = "testModuleWithlicense" + $testModuleName2 = "test_module_with_license" Get-NewPSResourceRepositoryFile Register-LocalRepos } @@ -394,7 +397,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidatio Install-PSResource -Name $testModuleName2 -Repository $TestGalleryName $pkg = Get-InstalledPSResource $testModuleName2 $pkg.Name | Should -Be $testModuleName2 - $pkg.Version | Should -Be "0.0.1.0" + $pkg.Version | Should -Be "2.0.0" } # This needs to be manually tested due to prompt @@ -408,4 +411,4 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidatio Set-PSResourceRepository PoshTestGallery -Trusted } -} \ No newline at end of file +} From c342f4329303d4ae78575ab18db3c8eb2387c7c4 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 31 May 2023 13:28:47 -0400 Subject: [PATCH 2/5] check JsonElement ValueKind for Tags to support it being string or array kind --- src/code/PSResourceInfo.cs | 20 ++++++++++++++++---- src/code/V3ServerAPICalls.cs | 20 ++++++++++++++------ 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 95690fbe1..dbdfa228c 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -658,12 +658,24 @@ public static bool TryConvertFromJson( // Tags if (rootDom.TryGetProperty("tags", out JsonElement tagsElement)) { - List tags = new List(); - foreach (var tag in tagsElement.EnumerateArray()) + string[] pkgTags = Utils.EmptyStrArray; + if (tagsElement.ValueKind == JsonValueKind.Array) { - tags.Add(tag.ToString()); + List tags = new List(); + foreach (var tag in tagsElement.EnumerateArray()) + { + tags.Add(tag.ToString()); + } + + pkgTags = tags.ToArray(); } - metadata["Tags"] = tags.ToArray(); + else if (tagsElement.ValueKind == JsonValueKind.String) + { + string tagStr = tagsElement.ToString(); + pkgTags = tagStr.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); + } + + metadata["Tags"] = pkgTags; } // PublishedDate diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index 38515b25d..9dc6652d2 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -361,6 +361,7 @@ private FindResults FindTagsFromNuGetRepo(string[] tags, bool includePrerelease, { string tagsQueryTerm = $"tags:{String.Join(" ", tags)}"; // Get responses for all packages that contain the required tags + // example query: JsonElement[] tagPkgEntries = GetVersionedPackageEntriesFromSearchQueryResource(tagsQueryTerm, includePrerelease, out edi); if (edi != null) { @@ -944,14 +945,21 @@ private bool IsRequiredTagSatisfied(JsonElement tagsElement, string[] tags, out // Get the package's tags from the tags JsonElement try { - List tagsFound = new List(); - JsonElement[] pkgTagElements = tagsElement.EnumerateArray().ToArray(); - foreach (JsonElement tagItem in pkgTagElements) + if (tagsElement.ValueKind == JsonValueKind.Array) { - tagsFound.Add(tagItem.ToString().ToLower()); - } + List tagsFound = new List(); + foreach (JsonElement tagItem in tagsElement.EnumerateArray()) + { + tagsFound.Add(tagItem.ToString()); + } - pkgTags = tagsFound.ToArray(); + pkgTags = tagsFound.ToArray(); + } + else if (tagsElement.ValueKind == JsonValueKind.String) + { + string tagStr = tagsElement.ToString(); + pkgTags = tagStr.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); + } } catch (Exception e) { From 8d3f8221631397863ad169badbf0b5a841ed3b31 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 31 May 2023 15:34:37 -0400 Subject: [PATCH 3/5] JsonDocument needs to account for implementing IDisposable --- src/code/V3ResponseUtil.cs | 23 ++- src/code/V3ServerAPICalls.cs | 271 +++++++++++++++++++---------------- 2 files changed, 157 insertions(+), 137 deletions(-) diff --git a/src/code/V3ResponseUtil.cs b/src/code/V3ResponseUtil.cs index 9b8f41b6d..fe49fc6d9 100644 --- a/src/code/V3ResponseUtil.cs +++ b/src/code/V3ResponseUtil.cs @@ -38,28 +38,27 @@ public override IEnumerable ConvertToPSResourceResult(FindResu string[] responses = responseResults.StringResponse; foreach (string response in responses) { - string parseError = String.Empty; - JsonDocument pkgVersionEntry = null; + string responseConversionError = String.Empty; + PSResourceInfo pkg = null; + try { - pkgVersionEntry = JsonDocument.Parse(response); + using (JsonDocument pkgVersionEntry = JsonDocument.Parse(response)) + { + PSResourceInfo.TryConvertFromJson(pkgVersionEntry, out pkg, Repository, out responseConversionError); + } } catch (Exception e) { - parseError = e.Message; - } - - if (!String.IsNullOrEmpty(parseError)) - { - yield return new PSResourceResult(returnedObject: null, errorMsg: parseError, isTerminatingError: false); + responseConversionError = e.Message; } - if (!PSResourceInfo.TryConvertFromJson(pkgVersionEntry, out PSResourceInfo psGetInfo, Repository, out string errorMsg)) + if (!String.IsNullOrEmpty(responseConversionError)) { - yield return new PSResourceResult(returnedObject: null, errorMsg: errorMsg, isTerminatingError: false); + yield return new PSResourceResult(returnedObject: null, errorMsg: responseConversionError, isTerminatingError: false); } - yield return new PSResourceResult(returnedObject: psGetInfo, errorMsg: String.Empty, isTerminatingError: false); + yield return new PSResourceResult(returnedObject: pkg, errorMsg: String.Empty, isTerminatingError: false); } } diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index 9dc6652d2..f1cb10286 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -179,15 +179,24 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange List satisfyingVersions = new List(); foreach (string response in versionedResponses) { - JsonElement pkgVersionElement; try { - JsonDocument pkgVersionEntry = JsonDocument.Parse(response); - JsonElement rootDom = pkgVersionEntry.RootElement; - if (!rootDom.TryGetProperty(versionName, out pkgVersionElement)) + using (JsonDocument pkgVersionEntry = JsonDocument.Parse(response)) { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain 'version' element.")); - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + + if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion) && versionRange.Satisfies(pkgVersion)) + { + if (!pkgVersion.IsPrerelease || includePrerelease) + { + satisfyingVersions.Add(response); + } + } } } catch (Exception e) @@ -195,14 +204,6 @@ public override FindResults FindVersionGlobbing(string packageName, VersionRange edi = ExceptionDispatchInfo.Capture(e); return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); } - - if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion) && versionRange.Satisfies(pkgVersion)) - { - if (!pkgVersion.IsPrerelease || includePrerelease) - { - satisfyingVersions.Add(response); - } - } } return new FindResults(stringResponse: satisfyingVersions.ToArray(), hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); @@ -394,27 +395,29 @@ private FindResults FindNameHelper(string packageName, string[] tags, bool inclu { try { - JsonDocument pkgVersionEntry = JsonDocument.Parse(response); - JsonElement rootDom = pkgVersionEntry.RootElement; - if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) - { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element for search with Name {packageName} in '{Repository.Name}'.")); - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - if (!rootDom.TryGetProperty(tagsName, out JsonElement tagsItem)) + using (JsonDocument pkgVersionEntry = JsonDocument.Parse(response)) { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{tagsName}' element for search with Name {packageName} in '{Repository.Name}'.")); - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element for search with Name {packageName} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + if (!rootDom.TryGetProperty(tagsName, out JsonElement tagsItem)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{tagsName}' element for search with Name {packageName} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } - if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) - { - if (!pkgVersion.IsPrerelease || includePrerelease) + if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) { - // Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 so grabbing the first match suffices - latestVersionResponse = response; - isTagMatch = IsRequiredTagSatisfied(tagsItem, tags, out edi); - break; + if (!pkgVersion.IsPrerelease || includePrerelease) + { + // Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 so grabbing the first match suffices + latestVersionResponse = response; + isTagMatch = IsRequiredTagSatisfied(tagsItem, tags, out edi); + break; + } } } } @@ -471,25 +474,27 @@ private FindResults FindVersionHelper(string packageName, string version, string // Versions are always in descending order i.e 5.0.0, 3.0.0, 1.0.0 try { - JsonDocument pkgVersionEntry = JsonDocument.Parse(response); - JsonElement rootDom = pkgVersionEntry.RootElement; - if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) - { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element for search with Name {packageName} and Version {version} in '{Repository.Name}'.")); - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - if (!rootDom.TryGetProperty(tagsName, out JsonElement tagsItem)) - { - edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{tagsName}' element for search with Name {packageName} and Version {version} in '{Repository.Name}'.")); - return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); - } - if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) + using (JsonDocument pkgVersionEntry = JsonDocument.Parse(response)) { - if (pkgVersion == requiredVersion) + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(versionName, out JsonElement pkgVersionElement)) + { + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{versionName}' element for search with Name {packageName} and Version {version} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + if (!rootDom.TryGetProperty(tagsName, out JsonElement tagsItem)) { - latestVersionResponse = response; - isTagMatch = IsRequiredTagSatisfied(tagsItem, tags, out edi); - break; + edi = ExceptionDispatchInfo.Capture(new InvalidOrEmptyResponse($"Response does not contain '{tagsName}' element for search with Name {packageName} and Version {version} in '{Repository.Name}'.")); + return new FindResults(stringResponse: Utils.EmptyStrArray, hashtableResponse: emptyHashResponses, responseType: v3FindResponseType); + } + if (NuGetVersion.TryParse(pkgVersionElement.ToString(), out NuGetVersion pkgVersion)) + { + if (pkgVersion == requiredVersion) + { + latestVersionResponse = response; + isTagMatch = IsRequiredTagSatisfied(tagsItem, tags, out edi); + break; + } } } } @@ -792,75 +797,75 @@ private string[] GetVersionedResponsesFromRegistrationsResource(string registrat try { // parse out JSON response we get from RegistrationsUrl - JsonDocument pkgVersionEntry = JsonDocument.Parse(pkgMappingResponse); - - // The response has a "items" array element, which only has useful 1st element - JsonElement rootDom = pkgVersionEntry.RootElement; - rootDom.TryGetProperty(itemsName, out JsonElement itemsElement); - if (itemsElement.GetArrayLength() == 0) + using (JsonDocument pkgVersionEntry = JsonDocument.Parse(pkgMappingResponse)) { - edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain '{itemsName}' element, for package with Name {packageName}.")); - return Utils.EmptyStrArray; - } - - JsonElement firstItem = itemsElement[0]; - - // https://api.nuget.org/v3/registration5-gz-semver2/test_module/index.json - // The "items" property contains an inner "items" element and a "count" element - // The inner "items" property is the metadata array for each version of the package. - // The "count" property represents how many versions are present for that package, (i.e how many elements are in the inner "items" array) + // The response has a "items" array element, which only has useful 1st element + JsonElement rootDom = pkgVersionEntry.RootElement; + if (!rootDom.TryGetProperty(itemsName, out JsonElement itemsElement) || itemsElement.GetArrayLength() == 0) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain '{itemsName}' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; + } - if (!firstItem.TryGetProperty(itemsName, out JsonElement innerItemsElements)) - { - edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{itemsName}' element, for package with Name {packageName}.")); - return Utils.EmptyStrArray; - } - - if (!firstItem.TryGetProperty(countName, out JsonElement countElement) || !countElement.TryGetInt32(out int count)) - { - edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{countName}' element or it is not a valid integer, for package with Name {packageName}.")); - return Utils.EmptyStrArray; - } + JsonElement firstItem = itemsElement[0]; - if (!firstItem.TryGetProperty("upper", out JsonElement upperVersionElement)) - { - edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner 'upper' element, for package with Name {packageName}.")); - return Utils.EmptyStrArray; - } + // https://api.nuget.org/v3/registration5-gz-semver2/test_module/index.json + // The "items" property contains an inner "items" element and a "count" element + // The inner "items" property is the metadata array for each version of the package. + // The "count" property represents how many versions are present for that package, (i.e how many elements are in the inner "items" array) - for (int i = 0; i < count; i++) - { - // Get the specific entry for each package version - JsonElement versionedItem = innerItemsElements[i]; - - // For search: - // The "catalogEntry" property in the specific package version entry contains package metadata - // For download: - // The "packageContent" property in the specific package version entry has the .nupkg URI for each version of the package. - if (!versionedItem.TryGetProperty(property, out JsonElement metadataElement)) + if (!firstItem.TryGetProperty(itemsName, out JsonElement innerItemsElements)) { - edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{property}' element, for package with Name {packageName}.")); + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{itemsName}' element, for package with Name {packageName}.")); return Utils.EmptyStrArray; } - versionedResponses.Add(metadataElement.ToString()); - } + if (!firstItem.TryGetProperty(countName, out JsonElement countElement) || !countElement.TryGetInt32(out int count)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{countName}' element or it is not a valid integer, for package with Name {packageName}.")); + return Utils.EmptyStrArray; + } - // Reverse array of versioned responses, if needed, so that version entries are in descending order. - string upperVersion = upperVersionElement.ToString(); - versionedResponseArr = versionedResponses.ToArray(); - if (isSearch) - { - if (!IsLatestVersionFirstForSearch(versionedResponseArr, upperVersion, out edi)) + if (!firstItem.TryGetProperty("upper", out JsonElement upperVersionElement)) { - Array.Reverse(versionedResponseArr); + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner 'upper' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; } - } - else - { - if (!IsLatestVersionFirstForInstall(versionedResponseArr, upperVersion, out edi)) + + for (int i = 0; i < count; i++) { - Array.Reverse(versionedResponseArr); + // Get the specific entry for each package version + JsonElement versionedItem = innerItemsElements[i]; + + // For search: + // The "catalogEntry" property in the specific package version entry contains package metadata + // For download: + // The "packageContent" property in the specific package version entry has the .nupkg URI for each version of the package. + if (!versionedItem.TryGetProperty(property, out JsonElement metadataElement)) + { + edi = ExceptionDispatchInfo.Capture(new ArgumentException($"Response does not contain inner '{property}' element, for package with Name {packageName}.")); + return Utils.EmptyStrArray; + } + + versionedResponses.Add(metadataElement.ToString()); + } + + // Reverse array of versioned responses, if needed, so that version entries are in descending order. + string upperVersion = upperVersionElement.ToString(); + versionedResponseArr = versionedResponses.ToArray(); + if (isSearch) + { + if (!IsLatestVersionFirstForSearch(versionedResponseArr, upperVersion, out edi)) + { + Array.Reverse(versionedResponseArr); + } + } + else + { + if (!IsLatestVersionFirstForInstall(versionedResponseArr, upperVersion, out edi)) + { + Array.Reverse(versionedResponseArr); + } } } } @@ -889,23 +894,33 @@ private bool IsLatestVersionFirstForSearch(string[] versionedResponses, string u } string firstResponse = versionedResponses[0]; - JsonDocument firstResponseJson = JsonDocument.Parse(firstResponse); - JsonElement firstResponseDom = firstResponseJson.RootElement; - if (!firstResponseDom.TryGetProperty(versionName, out JsonElement firstVersionElement)) - { - edi = ExceptionDispatchInfo.Capture(new JsonParsingException($"Response did not contain '{versionName}' element")); - return latestVersionFirst; - } - - string firstVersion = firstVersionElement.ToString(); - if (NuGetVersion.TryParse(upperVersion, out NuGetVersion upperPkgVersion) && NuGetVersion.TryParse(firstVersion, out NuGetVersion firstPkgVersion)) + try { - if (firstPkgVersion != upperPkgVersion) + using (JsonDocument firstResponseJson = JsonDocument.Parse(firstResponse)) { - latestVersionFirst = false; + JsonElement firstResponseDom = firstResponseJson.RootElement; + if (!firstResponseDom.TryGetProperty(versionName, out JsonElement firstVersionElement)) + { + edi = ExceptionDispatchInfo.Capture(new JsonParsingException($"Response did not contain '{versionName}' element")); + return latestVersionFirst; + } + + string firstVersion = firstVersionElement.ToString(); + if (NuGetVersion.TryParse(upperVersion, out NuGetVersion upperPkgVersion) && NuGetVersion.TryParse(firstVersion, out NuGetVersion firstPkgVersion)) + { + if (firstPkgVersion != upperPkgVersion) + { + latestVersionFirst = false; + } + } } } - + catch (Exception e) + { + edi = ExceptionDispatchInfo.Capture(e); + return true; + } + return latestVersionFirst; } @@ -987,7 +1002,8 @@ private bool IsRequiredTagSatisfied(JsonElement tagsElement, string[] tags, out /// private JsonElement[] GetJsonElementArr(string request, string propertyName, out ExceptionDispatchInfo edi) { - JsonElement[] pkgsArr = new JsonElement[0]; + List responseEntries = new List(); + JsonElement[] entries = new JsonElement[0]; try { string response = HttpRequestCall(request, out edi); @@ -996,11 +1012,16 @@ private JsonElement[] GetJsonElementArr(string request, string propertyName, out return new JsonElement[]{}; } - JsonDocument pkgsDom = JsonDocument.Parse(response); - - pkgsDom.RootElement.TryGetProperty(propertyName, out JsonElement pkgs); + using (JsonDocument pkgsDom = JsonDocument.Parse(response)) + { + pkgsDom.RootElement.TryGetProperty(propertyName, out JsonElement entryElement); + foreach (JsonElement entry in entryElement.EnumerateArray()) + { + responseEntries.Add(entry.Clone()); + } - pkgsArr = pkgs.EnumerateArray().ToArray(); + entries = responseEntries.ToArray(); + } } catch (Exception e) { @@ -1008,7 +1029,7 @@ private JsonElement[] GetJsonElementArr(string request, string propertyName, out edi = ExceptionDispatchInfo.Capture(new JsonParsingException(errMsg)); } - return pkgsArr; + return entries; } /// From 556abfcf38059acd6f66d23883183ddf2c8b334e Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 1 Jun 2023 09:51:21 -0400 Subject: [PATCH 4/5] PR feedback - track Dependencies via issue mentioned in comment and add const to Utils --- src/code/PSResourceInfo.cs | 4 ++-- src/code/Utils.cs | 1 + src/code/V3ServerAPICalls.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index dbdfa228c..c8bdf8e04 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -684,8 +684,8 @@ public static bool TryConvertFromJson( metadata["PublishedDate"] = ParseHttpDateTime(publishedElement.ToString()); } - // Dependencies - // TODO vNext, a little complicated + // Dependencies + // TODO, tracked via: https://github.com/PowerShell/PSResourceGet/issues/1169 // IsPrerelease // NuGet.org repository's response does contain 'isPrerelease' element so it can be accquired and set here. diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 63c43e975..58bc6a3ca 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -36,6 +36,7 @@ public enum MetadataFileType #region String fields public static readonly string[] EmptyStrArray = Array.Empty(); + public static readonly char[] SpaceSeparator = new char[]{' '}; public const string PSDataFileExt = ".psd1"; private const string ConvertJsonToHashtableScript = @" param ( diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index f1cb10286..e115cef08 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -973,7 +973,7 @@ private bool IsRequiredTagSatisfied(JsonElement tagsElement, string[] tags, out else if (tagsElement.ValueKind == JsonValueKind.String) { string tagStr = tagsElement.ToString(); - pkgTags = tagStr.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); + pkgTags = tagStr.Split(Utils.SpaceSeparator, StringSplitOptions.RemoveEmptyEntries); } } catch (Exception e) From 8bdeff2509742db4e7faf2a25898e14fdee05607 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 1 Jun 2023 10:09:06 -0400 Subject: [PATCH 5/5] PR feedback create list for tags with known array length --- src/code/PSResourceInfo.cs | 5 +++-- src/code/Utils.cs | 2 +- src/code/V3ServerAPICalls.cs | 5 +++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index c8bdf8e04..c212e4302 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -661,7 +661,8 @@ public static bool TryConvertFromJson( string[] pkgTags = Utils.EmptyStrArray; if (tagsElement.ValueKind == JsonValueKind.Array) { - List tags = new List(); + var arrayLength = tagsElement.GetArrayLength(); + List tags = new List(arrayLength); foreach (var tag in tagsElement.EnumerateArray()) { tags.Add(tag.ToString()); @@ -672,7 +673,7 @@ public static bool TryConvertFromJson( else if (tagsElement.ValueKind == JsonValueKind.String) { string tagStr = tagsElement.ToString(); - pkgTags = tagStr.Split(new char[]{' '}, StringSplitOptions.RemoveEmptyEntries); + pkgTags = tagStr.Split(Utils.WhitespaceSeparator, StringSplitOptions.RemoveEmptyEntries); } metadata["Tags"] = pkgTags; diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 58bc6a3ca..212fd602c 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -36,7 +36,7 @@ public enum MetadataFileType #region String fields public static readonly string[] EmptyStrArray = Array.Empty(); - public static readonly char[] SpaceSeparator = new char[]{' '}; + public static readonly char[] WhitespaceSeparator = new char[]{' '}; public const string PSDataFileExt = ".psd1"; private const string ConvertJsonToHashtableScript = @" param ( diff --git a/src/code/V3ServerAPICalls.cs b/src/code/V3ServerAPICalls.cs index e115cef08..ffce81a14 100644 --- a/src/code/V3ServerAPICalls.cs +++ b/src/code/V3ServerAPICalls.cs @@ -962,7 +962,8 @@ private bool IsRequiredTagSatisfied(JsonElement tagsElement, string[] tags, out { if (tagsElement.ValueKind == JsonValueKind.Array) { - List tagsFound = new List(); + var arrayLength = tagsElement.GetArrayLength(); + List tagsFound = new List(arrayLength); foreach (JsonElement tagItem in tagsElement.EnumerateArray()) { tagsFound.Add(tagItem.ToString()); @@ -973,7 +974,7 @@ private bool IsRequiredTagSatisfied(JsonElement tagsElement, string[] tags, out else if (tagsElement.ValueKind == JsonValueKind.String) { string tagStr = tagsElement.ToString(); - pkgTags = tagStr.Split(Utils.SpaceSeparator, StringSplitOptions.RemoveEmptyEntries); + pkgTags = tagStr.Split(Utils.WhitespaceSeparator, StringSplitOptions.RemoveEmptyEntries); } } catch (Exception e)