Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/code/InstallHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ private ConcurrentDictionary<string, Hashtable> InstallParentAndDependencyPackag
Parallel.ForEach(parentAndDeps, new ParallelOptions { MaxDegreeOfParallelism = maxDegreeOfParallelism }, depPkg =>
{
var depPkgName = depPkg.Name;
var depPkgVersion = depPkg.Version.ToString();
var depPkgVersion = Utils.GetFullVersionString(depPkg.Version.ToString(), depPkg.Prerelease);

verboseMsgs.Enqueue($"Installing package '{depPkgName}' version '{depPkgVersion}'");
//Stream responseStream = currentServer.InstallPackage(depPkgName, depPkgVersion, true, out ErrorRecord installNameErrRecord);
Expand Down
47 changes: 0 additions & 47 deletions src/code/PSResourceInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2162,51 +2162,4 @@ private static Dependency[] GetDependenciesForPsd1(Hashtable[] requiredModules)
}

#endregion

#region Test Hooks

public static class TestHooks
{
public static PSObject ReadPSGetResourceInfo(string filePath)
{
if (PSResourceInfo.TryRead(filePath, out PSResourceInfo psGetInfo, out string errorMsg))
{
return PSObject.AsPSObject(psGetInfo);
}

throw new PSInvalidOperationException(errorMsg);
}

public static PSObject ConvertFromJson(string json, PSRepositoryInfo repository)
{
using (JsonDocument pkgJson = JsonDocument.Parse(json))
{
if (PSResourceInfo.TryConvertFromJson(pkgJson, out PSResourceInfo psGetInfo, repository, out string errorMsg))
{
return PSObject.AsPSObject(psGetInfo);
}

throw new PSInvalidOperationException(errorMsg);
}
}

public static void WritePSGetResourceInfo(
string filePath,
PSObject psObjectGetInfo)
{
if (psObjectGetInfo.BaseObject is PSResourceInfo psGetInfo)
{
if (!psGetInfo.TryWrite(filePath, out string errorMsg))
{
throw new PSInvalidOperationException(errorMsg);
}

return;
}

throw new PSArgumentException("psObjectGetInfo argument is not a PSGetResourceInfo type.");
}
}

#endregion
}
72 changes: 72 additions & 0 deletions src/code/TestHooks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.PowerShell.PSResourceGet.Cmdlets;
using NuGet.Versioning;
using System.Management.Automation;
using System.Text.Json;

namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
{
/// <summary>
/// Entry points that expose internal functionality to the Pester tests.
/// </summary>
public static class TestHooks
{
public static PSObject ReadPSGetResourceInfo(string filePath)
{
if (PSResourceInfo.TryRead(filePath, out PSResourceInfo psGetInfo, out string errorMsg))
{
return PSObject.AsPSObject(psGetInfo);
}

throw new PSInvalidOperationException(errorMsg);
}

public static PSObject ConvertFromJson(string json, PSRepositoryInfo repository)
{
using (JsonDocument pkgJson = JsonDocument.Parse(json))
{
if (PSResourceInfo.TryConvertFromJson(pkgJson, out PSResourceInfo psGetInfo, repository, out string errorMsg))
{
return PSObject.AsPSObject(psGetInfo);
}

throw new PSInvalidOperationException(errorMsg);
}
}

public static void WritePSGetResourceInfo(
string filePath,
PSObject psObjectGetInfo)
{
if (psObjectGetInfo.BaseObject is PSResourceInfo psGetInfo)
{
if (!psGetInfo.TryWrite(filePath, out string errorMsg))
{
throw new PSInvalidOperationException(errorMsg);
}

return;
}

throw new PSArgumentException("psObjectGetInfo argument is not a PSGetResourceInfo type.");
}

/// <summary>
/// Selects the "packageContent" URL from V3 registration entries for the given version, or for the latest version if no version is given.
/// </summary>
public static string SelectV3PackageContentUrl(
string[] registrationEntries,
string version)
{
NuGetVersion requiredVersion = null;
if (!string.IsNullOrEmpty(version) && !NuGetVersion.TryParse(version, out requiredVersion))
{
throw new PSArgumentException($"Version '{version}' is not a valid NuGet version.");
}

return V3ServerAPICalls.GetPackageContentUrl(registrationEntries, requiredVersion);
}
}
}
187 changes: 94 additions & 93 deletions src/code/V3ServerAPICalls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -883,11 +883,6 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error
{
debugMsgs.Enqueue("In V3ServerAPICalls::InstallHelper()");
Stream pkgStream = null;
bool getLatestVersion = true;
if (version != null)
{
getLatestVersion = false;
}

string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, packageContentProperty, isSearch: false, out errRecord, errorMsgs, debugMsgs, verboseMsgs);
if (errRecord != null)
Expand All @@ -906,28 +901,10 @@ private Stream InstallHelper(string packageName, NuGetVersion version, out Error
return null;
}

string pkgContentUrl = String.Empty;
if (getLatestVersion)
{
pkgContentUrl = versionedResponses[0];
}
else
{
// loop through responses to find one containing required version
foreach (string response in versionedResponses)
{
// 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;
}
}
}

string pkgContentUrl = GetPackageContentUrl(versionedResponses, version);
if (String.IsNullOrEmpty(pkgContentUrl))
{
debugMsgs.Enqueue($"No registration entry for package '{packageName}' had a 'catalogEntry' version matching '{version}' and a 'packageContent' URL.");
errRecord = new ErrorRecord(
new Exception($"Package with name '{packageName}' and version '{version}' could not be found in repository '{Repository.Name}'"),
"InstallFailure",
Expand Down Expand Up @@ -966,11 +943,6 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
{
debugMsgs.Enqueue("In V3ServerAPICalls::InstallHelperAsync()");
Stream pkgStream = null;
bool getLatestVersion = true;
if (version != null)
{
getLatestVersion = false;
}

string[] versionedResponses = GetVersionedPackageEntriesFromRegistrationsResource(packageName, packageContentProperty, isSearch: false, out ErrorRecord errRecord, errorMsgs, debugMsgs, verboseMsgs);
if (errRecord != null)
Expand All @@ -990,28 +962,10 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
return null;
}

string pkgContentUrl = String.Empty;
if (getLatestVersion)
{
pkgContentUrl = versionedResponses[0];
}
else
{
// loop through responses to find one containing required version
foreach (string response in versionedResponses)
{
// 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;
}
}
}

string pkgContentUrl = GetPackageContentUrl(versionedResponses, version);
if (String.IsNullOrEmpty(pkgContentUrl))
{
debugMsgs.Enqueue($"No registration entry for package '{packageName}' had a 'catalogEntry' version matching '{version}' and a 'packageContent' URL.");
errorMsgs.Enqueue(new ErrorRecord(
new Exception($"Package with name '{packageName}' and version '{version}' could not be found in repository '{Repository.Name}'"),
"InstallFailure",
Expand Down Expand Up @@ -1039,6 +993,87 @@ private async Task<Stream> InstallHelperAsync(string packageName, NuGetVersion v
return pkgStream;
}

/// <summary>
/// Selects the "packageContent" URL (i.e the .nupkg download URL) of the registration entry for the required version,
/// or of the latest version if the required version is null.
/// The version is taken from the entry's "catalogEntry" > "version" property and compared as a NuGetVersion,
/// rather than being inferred from the URL text (a substring search for '1.2.3' would match the URL for '1.2.30').
/// </summary>
internal static string GetPackageContentUrl(string[] registrationEntries, NuGetVersion requiredVersion)
{
string pkgContentUrl = String.Empty;
if (registrationEntries == null)
{
return pkgContentUrl;
}

NuGetVersion latestVersion = null;
foreach (string registrationEntry in registrationEntries)
{
if (!TryGetPackageContentEntry(registrationEntry, out NuGetVersion entryVersion, out string entryPkgContentUrl))
{
continue;
}

if (requiredVersion != null)
{
if (entryVersion == requiredVersion)
{
return entryPkgContentUrl;
}
}
else if (latestVersion == null || entryVersion > latestVersion)
{
latestVersion = entryVersion;
pkgContentUrl = entryPkgContentUrl;
}
}

return pkgContentUrl;
}

/// <summary>
/// Gets the version and "packageContent" URL from a registration entry, which looks like:
/// { "catalogEntry": { "version": "1.0.0", ... }, "packageContent": "https://api.nuget.org/v3-flatcontainer/test_module/1.0.0/test_module.1.0.0.nupkg", ... }
/// </summary>
private static bool TryGetPackageContentEntry(string registrationEntry, out NuGetVersion version, out string pkgContentUrl)
{
version = null;
pkgContentUrl = String.Empty;
if (String.IsNullOrWhiteSpace(registrationEntry))
{
return false;
}

try
{
using (JsonDocument registrationEntryJson = JsonDocument.Parse(registrationEntry))
{
JsonElement rootDom = registrationEntryJson.RootElement;
if (rootDom.ValueKind != JsonValueKind.Object ||
!rootDom.TryGetProperty(packageContentProperty, out JsonElement pkgContentElement) ||
pkgContentElement.ValueKind != JsonValueKind.String ||
!rootDom.TryGetProperty(catalogEntryProperty, out JsonElement catalogEntryElement) ||
catalogEntryElement.ValueKind != JsonValueKind.Object ||
!catalogEntryElement.TryGetProperty(versionName, out JsonElement versionElement) ||
!NuGetVersion.TryParse(versionElement.ToString(), out version))
{
version = null;
return false;
}

pkgContentUrl = pkgContentElement.GetString();
}
}
catch (JsonException)
{
version = null;
return false;
}

return !String.IsNullOrWhiteSpace(pkgContentUrl);
}

/// <summary>
/// Gets the versioned package entries from the RegistrationsBaseUrl resource
/// i.e when the package Name being searched for does not contain wildcard
Expand Down Expand Up @@ -1451,8 +1486,9 @@ private string[] GetMetadataElementsFromResponse(string response, string propert

if (metadataElement.ValueKind == JsonValueKind.String)
{
// This is when property is "packageContent"
versionedPkgResponses.Add(metadataElement.ToString());
// This is when property is "packageContent".
// The whole entry is returned so the version can be read from its "catalogEntry" rather than inferred from the URL.
versionedPkgResponses.Add(item.ToString());
}
else if(metadataElement.ValueKind == JsonValueKind.Object)
{
Expand Down Expand Up @@ -1494,7 +1530,8 @@ private string[] GetMetadataElementsFromResponse(string response, string propert
/// This contains an inner items element (containing the package metadata) and the packageContent element (containing URI through which the .nupkg can be downloaded)
/// <param name="property"> 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.
/// The "packageContent" property is used for download, and the value is the whole registration entry,
/// containing both the "catalogEntry" (with the package version) and the "packageContent" URI for the .nupkg file.
/// </param>
/// <summary>
private string[] GetVersionedResponsesFromRegistrationsResource(string registrationsBaseUrl, string packageName, string property, bool isSearch, out ErrorRecord errRecord, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
Expand Down Expand Up @@ -1526,25 +1563,16 @@ private string[] GetVersionedResponsesFromRegistrationsResource(string registrat
}

// Reverse array of versioned responses, if needed, so that version entries are in descending order.
if (String.IsNullOrEmpty(upperVersion))
// Install selects its entry by comparing versions, so it does not depend on the order.
if (String.IsNullOrEmpty(upperVersion) || !isSearch)
{
// add write Debug and use these results
return versionedResponseArr;
}

if (isSearch)
if (!IsLatestVersionFirstForSearch(versionedResponseArr, out errRecord, errorMsgs, debugMsgs, verboseMsgs))
{
if (!IsLatestVersionFirstForSearch(versionedResponseArr, out errRecord, errorMsgs, debugMsgs, verboseMsgs))
{
Array.Reverse(versionedResponseArr);
}
}
else
{
if (!IsLatestVersionFirstForInstall(versionedResponseArr, upperVersion, out errRecord, errorMsgs, debugMsgs, verboseMsgs))
{
Array.Reverse(versionedResponseArr);
}
Array.Reverse(versionedResponseArr);
}

return versionedResponseArr;
Expand Down Expand Up @@ -1649,33 +1677,6 @@ private bool IsLatestVersionFirstForSearch(string[] versionedResponses, out Erro
return latestVersionFirst;
}

/// <summary>
/// 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.
/// Entries do not reflect prerelease preference so all versions (including prerelease) are being considered here, so upper version (including prerelease) can be used for comparison.
/// </summary>
private bool IsLatestVersionFirstForInstall(string[] versionedResponses, string upperVersion, out ErrorRecord errRecord, ConcurrentQueue<ErrorRecord> errorMsgs, ConcurrentQueue<string> debugMsgs, ConcurrentQueue<string> verboseMsgs)
{
debugMsgs.Enqueue("In V3ServerAPICalls::IsLatestVersionFirstForInstall()");
errRecord = null;
bool latestVersionFirst = true;

// We don't need to perform this check if no responses, or single response
if (versionedResponses.Length < 2)
{
return latestVersionFirst;
}

string firstResponse = versionedResponses[0];
// for Install, response will be a URI value for the package .nupkg, not JSON
if (!firstResponse.Contains(upperVersion))
{
latestVersionFirst = false;
}

return latestVersionFirst;
}

/// <summary>
/// Helper method that determines if specified tags are present in package's tags.
/// </summary>
Expand Down
Loading
Loading