diff --git a/src/NuGetGallery.Core/SemVerLevelKey.cs b/src/NuGetGallery.Core/SemVerLevelKey.cs
index 33ba34eef7..c57f7b2707 100644
--- a/src/NuGetGallery.Core/SemVerLevelKey.cs
+++ b/src/NuGetGallery.Core/SemVerLevelKey.cs
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
+using System.Linq.Expressions;
using NuGet.Versioning;
namespace NuGetGallery
@@ -15,6 +16,8 @@ namespace NuGetGallery
///
public static class SemVerLevelKey
{
+ private static readonly NuGetVersion _semVer2Version = NuGetVersion.Parse("2.0.0");
+
///
/// This could either indicate being SemVer1-compliant, or non-SemVer-compliant at all (e.g. System.Versioning pattern).
///
@@ -31,7 +34,7 @@ public static class SemVerLevelKey
///
/// The package's non-normalized, original version string.
/// The package's direct dependencies as defined in the package's manifest.
- /// Returns null when unknown; otherwise the identified SemVer-level.
+ /// Returns null when unknown; otherwise the identified SemVer-level key.
public static int? ForPackage(NuGetVersion originalVersion, IEnumerable dependencies)
{
if (originalVersion == null)
@@ -65,5 +68,52 @@ public static class SemVerLevelKey
return Unknown;
}
+
+ ///
+ /// Identifies the SemVer-level for a given semVerLevel version string.
+ ///
+ /// The version string indicating the supported SemVer-level.
+ ///
+ /// Returns null when unknown; otherwise the identified SemVer-level key.
+ ///
+ ///
+ /// Older clients don't send the semVerLevel query parameter at all,
+ /// so we default to Unknown for backwards-compatibility.
+ ///
+ public static int? ForSemVerLevel(string semVerLevel)
+ {
+ if (semVerLevel == null)
+ {
+ return Unknown;
+ }
+
+ NuGetVersion parsedVersion;
+ if (NuGetVersion.TryParse(semVerLevel, out parsedVersion))
+ {
+ return _semVer2Version <= parsedVersion ? SemVer2 : Unknown;
+ }
+ else
+ {
+ return Unknown;
+ }
+ }
+
+ ///
+ /// Indicates whether the provided SemVer-level key is compliant with the provided SemVer-level version string.
+ ///
+ /// The SemVer-level string indicating the SemVer-level to comply with.
+ /// True if compliant; otherwise false.
+ public static Expression> IsPackageCompliantWithSemVerLevel(string semVerLevel)
+ {
+ // Note: we must return an expression that Linq to Entities is able to translate to SQL
+ var parsedSemVerLevelKey = ForSemVerLevel(semVerLevel);
+
+ if (parsedSemVerLevelKey == SemVer2)
+ {
+ return p => p.SemVerLevelKey == Unknown || p.SemVerLevelKey == parsedSemVerLevelKey;
+ }
+
+ return p => p.SemVerLevelKey == Unknown;
+ }
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/App_Start/NuGetODataV2FeedConfig.cs b/src/NuGetGallery/App_Start/NuGetODataV2FeedConfig.cs
index b2b52dc552..3a5983afa7 100644
--- a/src/NuGetGallery/App_Start/NuGetODataV2FeedConfig.cs
+++ b/src/NuGetGallery/App_Start/NuGetODataV2FeedConfig.cs
@@ -60,6 +60,7 @@ public static IEdmModel GetEdmModel()
searchAction.Parameter("searchTerm");
searchAction.Parameter("targetFramework");
searchAction.Parameter("includePrerelease");
+ searchAction.Parameter("semVerLevel");
searchAction.ReturnsCollectionFromEntitySet("Packages");
var findPackagesAction = builder.Action("FindPackagesById");
@@ -73,6 +74,7 @@ public static IEdmModel GetEdmModel()
getUpdatesAction.Parameter("includeAllVersions");
getUpdatesAction.Parameter("targetFrameworks");
getUpdatesAction.Parameter("versionConstraints");
+ getUpdatesAction.Parameter("semVerLevel");
getUpdatesAction.ReturnsCollectionFromEntitySet("Packages");
var model = builder.GetEdmModel();
diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs
index d09a38f76d..e510cb297b 100644
--- a/src/NuGetGallery/Controllers/ApiController.cs
+++ b/src/NuGetGallery/Controllers/ApiController.cs
@@ -121,7 +121,7 @@ public virtual async Task GetPackage(string id, string version)
// some security paranoia about URL hacking somehow creating e.g. open redirects
// validate user input: explicit calls to the same validators used during Package Registrations
// Ideally shouldn't be necessary?
- if (!PackageIdValidator.IsValidPackageId(id ?? ""))
+ if (!PackageIdValidator.IsValidPackageId(id ?? string.Empty))
{
return new HttpStatusCodeWithBodyResult(HttpStatusCode.BadRequest, "The format of the package id is invalid");
}
@@ -632,24 +632,30 @@ protected internal virtual Stream ReadPackageFromRequest()
[HttpGet]
[ActionName("PackageIDs")]
- public virtual async Task GetPackageIds(string partialId, bool? includePrerelease)
+ public virtual async Task GetPackageIds(
+ string partialId,
+ bool? includePrerelease,
+ string semVerLevel = null)
{
var query = GetService();
return new JsonResult
{
- Data = (await query.Execute(partialId, includePrerelease)).ToArray(),
+ Data = (await query.Execute(partialId, includePrerelease, semVerLevel)).ToArray(),
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
[HttpGet]
[ActionName("PackageVersions")]
- public virtual async Task GetPackageVersions(string id, bool? includePrerelease)
+ public virtual async Task GetPackageVersions(
+ string id,
+ bool? includePrerelease,
+ string semVerLevel = null)
{
var query = GetService();
return new JsonResult
{
- Data = (await query.Execute(id, includePrerelease)).ToArray(),
+ Data = (await query.Execute(id, includePrerelease, semVerLevel)).ToArray(),
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
}
diff --git a/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs b/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs
index 4a6659ec7e..606f6b8bba 100644
--- a/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs
+++ b/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs
@@ -40,11 +40,14 @@ public ODataV2CuratedFeedController(
_curatedFeedService = curatedFeedService;
}
- // /api/v2/curated-feed/curatedFeedName/Packages
+ // /api/v2/curated-feed/curatedFeedName/Packages?semVerLevel=
[HttpGet]
[HttpPost]
[CacheOutput(NoCache = true)]
- public IHttpActionResult Get(ODataQueryOptions options, string curatedFeedName)
+ public IHttpActionResult Get(
+ ODataQueryOptions options,
+ string curatedFeedName,
+ [FromUri] string semVerLevel = null)
{
if (!_entities.CuratedFeeds.Any(cf => cf.Name == curatedFeedName))
{
@@ -52,19 +55,22 @@ public IHttpActionResult Get(ODataQueryOptions options, string cu
}
var queryable = _curatedFeedService.GetPackages(curatedFeedName)
- .Where(p => p.SemVerLevelKey == SemVerLevelKey.Unknown)
+ .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel))
.ToV2FeedPackageQuery(_configurationService.GetSiteRoot(UseHttps()), _configurationService.Features.FriendlyLicenses)
.InterceptWith(new NormalizeVersionInterceptor());
return QueryResult(options, queryable, MaxPageSize);
}
- // /api/v2/curated-feed/curatedFeedName/Packages/$count
+ // /api/v2/curated-feed/curatedFeedName/Packages/$count?semVerLevel=
[HttpGet]
[CacheOutput(NoCache = true)]
- public IHttpActionResult GetCount(ODataQueryOptions options, string curatedFeedName)
+ public IHttpActionResult GetCount(
+ ODataQueryOptions options,
+ string curatedFeedName,
+ [FromUri] string semVerLevel = null)
{
- return Get(options, curatedFeedName).FormattedAsCountResult();
+ return Get(options, curatedFeedName, semVerLevel).FormattedAsCountResult();
}
// /api/v2/curated-feed/curatedFeedName/Packages(Id=,Version=)
@@ -72,15 +78,19 @@ public IHttpActionResult GetCount(ODataQueryOptions options, stri
[CacheOutput(ServerTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds, Private = true, ClientTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds)]
public async Task Get(ODataQueryOptions options, string curatedFeedName, string id, string version)
{
- var result = await GetCore(options, curatedFeedName, id, version, return404NotFoundWhenNoResults: true);
+ var result = await GetCore(options, curatedFeedName, id, version, return404NotFoundWhenNoResults: true, semVerLevel: null);
return result.FormattedAsSingleResult();
}
- // /api/v2/curated-feed/curatedFeedName/FindPackagesById()?id=
+ // /api/v2/curated-feed/curatedFeedName/FindPackagesById()?id=&semVerLevel=
[HttpGet]
[HttpPost]
[CacheOutput(ServerTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds, Private = true, ClientTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds)]
- public async Task FindPackagesById(ODataQueryOptions options, string curatedFeedName, [FromODataUri]string id)
+ public async Task FindPackagesById(
+ ODataQueryOptions options,
+ string curatedFeedName,
+ [FromODataUri] string id,
+ [FromUri] string semVerLevel = null)
{
if (string.IsNullOrEmpty(curatedFeedName) || string.IsNullOrEmpty(id))
{
@@ -90,10 +100,16 @@ public async Task FindPackagesById(ODataQueryOptions GetCore(ODataQueryOptions options, string curatedFeedName, string id, string version, bool return404NotFoundWhenNoResults)
+ private async Task GetCore(
+ ODataQueryOptions options,
+ string curatedFeedName,
+ string id,
+ string version,
+ bool return404NotFoundWhenNoResults,
+ string semVerLevel)
{
var curatedFeed = _entities.CuratedFeeds.FirstOrDefault(cf => cf.Name == curatedFeedName);
if (curatedFeed == null)
@@ -102,8 +118,8 @@ private async Task GetCore(ODataQueryOptions o
}
var packages = _curatedFeedService.GetPackages(curatedFeedName)
- .Where(p => p.SemVerLevelKey == SemVerLevelKey.Unknown
- && p.PackageRegistration.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
+ .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel))
+ .Where(p => p.PackageRegistration.Id.Equals(id, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(version))
{
@@ -167,7 +183,7 @@ public IHttpActionResult GetPropertyFromPackages(string propertyName, string id,
return BadRequest("Querying property " + propertyName + " is not supported.");
}
- // /api/v2/curated-feed/curatedFeedName/Search()?searchTerm=&targetFramework=&includePrerelease=
+ // /api/v2/curated-feed/curatedFeedName/Search()?searchTerm=&targetFramework=&includePrerelease=&semVerLevel=
[HttpGet]
[HttpPost]
[CacheOutput(ServerTimeSpan = NuGetODataConfig.SearchCacheTimeInSeconds, ClientTimeSpan = NuGetODataConfig.SearchCacheTimeInSeconds)]
@@ -176,7 +192,8 @@ public async Task Search(
string curatedFeedName,
[FromODataUri]string searchTerm = "",
[FromODataUri]string targetFramework = "",
- [FromODataUri]bool includePrerelease = false)
+ [FromODataUri]bool includePrerelease = false,
+ [FromUri]string semVerLevel = null)
{
if (!_entities.CuratedFeeds.Any(cf => cf.Name == curatedFeedName))
{
@@ -203,7 +220,7 @@ public async Task Search(
// Perform actual search
var curatedFeed = _curatedFeedService.GetFeedByName(curatedFeedName, includePackages: false);
var packages = _curatedFeedService.GetPackages(curatedFeedName)
- .Where(p => p.SemVerLevelKey == SemVerLevelKey.Unknown)
+ .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel))
.OrderBy(p => p.PackageRegistration.Id).ThenBy(p => p.Version);
// todo: search hijack should take queryOptions instead of manually parsing query options
@@ -247,9 +264,10 @@ public async Task SearchCount(
string curatedFeedName,
[FromODataUri]string searchTerm = "",
[FromODataUri]string targetFramework = "",
- [FromODataUri]bool includePrerelease = false)
+ [FromODataUri]bool includePrerelease = false,
+ [FromUri]string semVerLevel = null)
{
- var searchResults = await Search(options, curatedFeedName, searchTerm, targetFramework, includePrerelease);
+ var searchResults = await Search(options, curatedFeedName, searchTerm, targetFramework, includePrerelease, semVerLevel);
return searchResults.FormattedAsCountResult();
}
}
diff --git a/src/NuGetGallery/Controllers/ODataV2FeedController.cs b/src/NuGetGallery/Controllers/ODataV2FeedController.cs
index de7b417689..b6fefd5523 100644
--- a/src/NuGetGallery/Controllers/ODataV2FeedController.cs
+++ b/src/NuGetGallery/Controllers/ODataV2FeedController.cs
@@ -43,18 +43,21 @@ public ODataV2FeedController(
_searchService = searchService;
}
- // /api/v2/Packages
+ // /api/v2/Packages?semVerLevel=
[HttpGet]
[HttpPost]
[CacheOutput(NoCache = true)]
- public async Task Get(ODataQueryOptions options)
+ public async Task Get(
+ ODataQueryOptions options,
+ [FromUri]string semVerLevel = null)
{
// Setup the search
var packages = _packagesRepository.GetAll()
- .Where(p => !p.Deleted && p.SemVerLevelKey == SemVerLevelKey.Unknown)
+ .Where(p => !p.Deleted)
+ .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel))
.WithoutSortOnColumn(Version)
.WithoutSortOnColumn(Id, ShouldIgnoreOrderById(options))
- .InterceptWith(new NormalizeVersionInterceptor()) ;
+ .InterceptWith(new NormalizeVersionInterceptor());
// Try the search service
try
@@ -90,7 +93,7 @@ public async Task Get(ODataQueryOptions option
QuietLog.LogHandledException(ex);
}
- //Reject only when try to reach database.
+ // Reject only when try to reach database.
if (!ODataQueryVerifier.AreODataOptionsAllowed(options, ODataQueryVerifier.V2Packages,
_configurationService.Current.IsODataFilterEnabled, nameof(Get)))
{
@@ -101,28 +104,39 @@ public async Task Get(ODataQueryOptions option
return QueryResult(options, queryable, MaxPageSize);
}
- // /api/v2/Packages/$count
+ // /api/v2/Packages/$count?semVerLevel=
[HttpGet]
[CacheOutput(NoCache = true)]
- public async Task GetCount(ODataQueryOptions options)
+ public async Task GetCount(
+ ODataQueryOptions options,
+ [FromUri]string semVerLevel = null)
{
- return (await Get(options)).FormattedAsCountResult();
+ return (await Get(options, semVerLevel)).FormattedAsCountResult();
}
// /api/v2/Packages(Id=,Version=)
[HttpGet]
[CacheOutput(ServerTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds, Private = true, ClientTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds)]
- public async Task Get(ODataQueryOptions options, string id, string version)
+ public async Task Get(
+ ODataQueryOptions options,
+ string id,
+ string version)
{
- var result = await GetCore(options, id, version, return404NotFoundWhenNoResults: true);
+ // We are defaulting to semVerLevel = "2.0.0" by design.
+ // The client is requesting a specific package version and should support what it requests.
+ // If not, too bad :)
+ var result = await GetCore(options, id, version, semVerLevel: "2.0.0", return404NotFoundWhenNoResults: true);
return result.FormattedAsSingleResult();
}
- // /api/v2/FindPackagesById()?id=
+ // /api/v2/FindPackagesById()?id=&semVerLevel=
[HttpGet]
[HttpPost]
[CacheOutput(ServerTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds, Private = true, ClientTimeSpan = NuGetODataConfig.GetByIdAndVersionCacheTimeInSeconds)]
- public async Task FindPackagesById(ODataQueryOptions options, [FromODataUri]string id)
+ public async Task FindPackagesById(
+ ODataQueryOptions options,
+ [FromODataUri]string id,
+ [FromUri]string semVerLevel = null)
{
if (string.IsNullOrEmpty(id))
{
@@ -132,18 +146,32 @@ public async Task FindPackagesById(ODataQueryOptions GetCore(ODataQueryOptions options, string id, string version, bool return404NotFoundWhenNoResults)
+ private async Task GetCore(
+ ODataQueryOptions options,
+ string id,
+ string version,
+ string semVerLevel,
+ bool return404NotFoundWhenNoResults)
{
var packages = _packagesRepository.GetAll()
.Include(p => p.PackageRegistration)
- .Where(p => p.PackageRegistration.Id.Equals(id, StringComparison.OrdinalIgnoreCase) && !p.Deleted && p.SemVerLevelKey == SemVerLevelKey.Unknown);
+ .Where(p => p.PackageRegistration.Id.Equals(id, StringComparison.OrdinalIgnoreCase)
+ && !p.Deleted)
+ .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel));
if (!string.IsNullOrEmpty(version))
{
- packages = packages.Where(p => p.Version == version);
+ NuGetVersion nugetVersion;
+ if (NuGetVersion.TryParse(version, out nugetVersion))
+ {
+ // Our APIs expect to receive normalized version strings.
+ // We need to compare normalized versions or we can never retrieve SemVer2 package versions.
+ var normalizedString = nugetVersion.ToNormalizedString();
+ packages = packages.Where(p => p.NormalizedVersion == normalizedString);
+ }
}
// try the search service
@@ -211,7 +239,8 @@ public async Task Search(
ODataQueryOptions options,
[FromODataUri]string searchTerm = "",
[FromODataUri]string targetFramework = "",
- [FromODataUri]bool includePrerelease = false)
+ [FromODataUri]bool includePrerelease = false,
+ [FromUri]string semVerLevel = null)
{
// Handle OData-style |-separated list of frameworks.
string[] targetFrameworkList = (targetFramework ?? "").Split(new[] { '\'', '|' }, StringSplitOptions.RemoveEmptyEntries);
@@ -234,7 +263,8 @@ public async Task Search(
var packages = _packagesRepository.GetAll()
.Include(p => p.PackageRegistration)
.Include(p => p.PackageRegistration.Owners)
- .Where(p => p.Listed && !p.Deleted && p.SemVerLevelKey == SemVerLevelKey.Unknown)
+ .Where(p => p.Listed && !p.Deleted)
+ .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel))
.OrderBy(p => p.PackageRegistration.Id).ThenBy(p => p.Version)
.AsNoTracking();
@@ -277,20 +307,21 @@ public async Task Search(
return QueryResult(options, queryable, MaxPageSize);
}
- // /api/v2/Search()/$count?searchTerm=&targetFramework=&includePrerelease=
+ // /api/v2/Search()/$count?searchTerm=&targetFramework=&includePrerelease=&semVerLevel=
[HttpGet]
[CacheOutput(ServerTimeSpan = NuGetODataConfig.SearchCacheTimeInSeconds, ClientTimeSpan = NuGetODataConfig.SearchCacheTimeInSeconds)]
public async Task SearchCount(
ODataQueryOptions options,
[FromODataUri]string searchTerm = "",
[FromODataUri]string targetFramework = "",
- [FromODataUri]bool includePrerelease = false)
+ [FromODataUri]bool includePrerelease = false,
+ [FromUri]string semVerLevel = null)
{
- var searchResults = await Search(options, searchTerm, targetFramework, includePrerelease);
+ var searchResults = await Search(options, searchTerm, targetFramework, includePrerelease, semVerLevel);
return searchResults.FormattedAsCountResult();
}
- // /api/v2/GetUpdates()?packageIds=&versions=&includePrerelease=&includeAllVersions=&targetFrameworks=&versionConstraints=
+ // /api/v2/GetUpdates()?packageIds=&versions=&includePrerelease=&includeAllVersions=&targetFrameworks=&versionConstraints=&semVerLevel=
[HttpGet]
[HttpPost]
public IHttpActionResult GetUpdates(
@@ -300,7 +331,8 @@ public IHttpActionResult GetUpdates(
[FromODataUri]bool includePrerelease,
[FromODataUri]bool includeAllVersions,
[FromODataUri]string targetFrameworks = "",
- [FromODataUri]string versionConstraints = "")
+ [FromODataUri]string versionConstraints = "",
+ [FromUri]string semVerLevel = null)
{
if (string.IsNullOrEmpty(packageIds) || string.IsNullOrEmpty(versions))
{
@@ -365,14 +397,14 @@ public IHttpActionResult GetUpdates(
idValues.Contains(p.PackageRegistration.Id.ToLower()))
.OrderBy(p => p.PackageRegistration.Id);
- var queryable = GetUpdates(packages, versionLookup, targetFrameworkValues, includeAllVersions)
+ var queryable = GetUpdates(packages, versionLookup, targetFrameworkValues, includeAllVersions, semVerLevel)
.AsQueryable()
.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses);
return QueryResult(options, queryable, MaxPageSize);
}
- // /api/v2/GetUpdates()/$count?packageIds=&versions=&includePrerelease=&includeAllVersions=&targetFrameworks=&versionConstraints=
+ // /api/v2/GetUpdates()/$count?packageIds=&versions=&includePrerelease=&includeAllVersions=&targetFrameworks=&versionConstraints=&semVerLevel=
[HttpGet]
[HttpPost]
public IHttpActionResult GetUpdatesCount(
@@ -382,9 +414,10 @@ public IHttpActionResult GetUpdatesCount(
[FromODataUri]bool includePrerelease,
[FromODataUri]bool includeAllVersions,
[FromODataUri]string targetFrameworks = "",
- [FromODataUri]string versionConstraints = "")
+ [FromODataUri]string versionConstraints = "",
+ [FromUri]string semVerLevel = null)
{
- return GetUpdates(options, packageIds, versions, includePrerelease, includeAllVersions, targetFrameworks, versionConstraints)
+ return GetUpdates(options, packageIds, versions, includePrerelease, includeAllVersions, targetFrameworks, versionConstraints, semVerLevel)
.FormattedAsCountResult();
}
@@ -392,11 +425,14 @@ private static IEnumerable GetUpdates(
IEnumerable packages,
ILookup> versionLookup,
IEnumerable targetFrameworkValues,
- bool includeAllVersions)
+ bool includeAllVersions,
+ string semVerLevel)
{
+ var isSemVerLevelCompliant = SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel).Compile();
+
var updates = from p in packages.AsEnumerable()
let version = NuGetVersion.Parse(p.Version)
- where p.SemVerLevelKey == SemVerLevelKey.Unknown
+ where isSemVerLevelCompliant(p)
&& versionLookup[p.PackageRegistration.Id].Any(versionTuple =>
{
NuGetVersion clientVersion = versionTuple.Item1;
@@ -418,6 +454,7 @@ private static IEnumerable GetUpdates(
updates = updates.GroupBy(p => p.PackageRegistration.Id)
.Select(g => g.OrderByDescending(p => NuGetVersion.Parse(p.Version)).First());
}
+
return updates;
}
}
diff --git a/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs b/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs
index 7c18883fef..5d9349338c 100644
--- a/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs
+++ b/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs
@@ -68,6 +68,10 @@ private void TryAnnotateV2FeedPackage(ODataEntry entry, EntityInstanceContext en
var instance = entityInstanceContext.EntityInstance as V2FeedPackage;
if (instance != null)
{
+ // Patch links to use normalized versions
+ var normalizedVersion = NuGetVersionNormalizer.Normalize(instance.Version);
+ NormalizeNavigationLinks(entry, entityInstanceContext.Request, instance, normalizedVersion);
+
// Set Atom entry metadata
var atomEntryMetadata = new AtomEntryMetadata();
atomEntryMetadata.Title = instance.Id;
@@ -89,11 +93,31 @@ private void TryAnnotateV2FeedPackage(ODataEntry entry, EntityInstanceContext en
entry.MediaResource = new ODataStreamReferenceValue
{
ContentType = ContentType,
- ReadLink = BuildLinkForStreamProperty("v2", instance.Id, instance.Version, entityInstanceContext.Request)
+ ReadLink = BuildLinkForStreamProperty("v2", instance.Id, normalizedVersion, entityInstanceContext.Request)
};
}
}
+ private static void NormalizeNavigationLinks(ODataEntry entry, HttpRequestMessage request, V2FeedPackage instance, string normalizedVersion)
+ {
+ var idLink = BuildIdLink("v2", instance.Id, normalizedVersion, request);
+
+ if (entry.ReadLink != null)
+ {
+ entry.ReadLink = idLink;
+ }
+
+ if (entry.EditLink != null)
+ {
+ entry.EditLink = idLink;
+ }
+
+ if (entry.Id != null)
+ {
+ entry.Id = idLink.ToString();
+ }
+ }
+
public string ContentType
{
get { return _contentType; }
@@ -111,6 +135,11 @@ private static Uri BuildLinkForStreamProperty(string routePrefix, string id, str
return builder.Uri;
}
+ private static Uri BuildIdLink(string routePrefix, string id, string version, HttpRequestMessage request)
+ {
+ return new Uri($"{request.RequestUri.Scheme}://{request.RequestUri.Host}/api/{routePrefix}/Packages(Id='{id}',Version='{version}')");
+ }
+
private static string EnsureTrailingSlash(string url)
{
if (url != null && !url.EndsWith("/", StringComparison.OrdinalIgnoreCase))
diff --git a/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs
index 4392f5f115..1a59b901c9 100644
--- a/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs
+++ b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageIdsQuery.cs
@@ -13,15 +13,15 @@ public class AutoCompleteDatabasePackageIdsQuery
private const string _partialIdSqlFormat = @"SELECT TOP 30 pr.ID
FROM Packages p (NOLOCK)
JOIN PackageRegistrations pr (NOLOCK) on pr.[Key] = p.PackageRegistrationKey
-WHERE p.[SemVerLevelKey] IS NULL AND pr.ID LIKE {{0}}
- {0}
+WHERE {0} AND pr.ID LIKE {{0}}
+ {1}
GROUP BY pr.ID
ORDER BY pr.ID";
private const string _noPartialIdSql = @"SELECT TOP 30 pr.ID
FROM Packages p (NOLOCK)
JOIN PackageRegistrations pr (NOLOCK) on pr.[Key] = p.PackageRegistrationKey
-WHERE p.[SemVerLevelKey] IS NULL
+WHERE {0}
GROUP BY pr.ID
ORDER BY MAX(pr.DownloadCount) DESC";
@@ -32,11 +32,24 @@ public AutoCompleteDatabasePackageIdsQuery(IEntitiesContext entities)
public Task> Execute(
string partialId,
- bool? includePrerelease = false)
+ bool? includePrerelease = false,
+ string semVerLevel = null)
{
+ // Create SQL filter on SemVerLevel
+ // By default, we filter out SemVer v2.0.0 package versions.
+ var semVerLevelSqlFilter = "p.[SemVerLevelKey] IS NULL";
+ if (!string.IsNullOrEmpty(semVerLevel))
+ {
+ var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel);
+ if (semVerLevelKey == SemVerLevelKey.SemVer2)
+ {
+ semVerLevelSqlFilter = "p.[SemVerLevelKey] = " + SemVerLevelKey.SemVer2;
+ }
+ }
+
if (string.IsNullOrWhiteSpace(partialId))
{
- return RunQuery(_noPartialIdSql);
+ return RunSqlQuery(string.Format(CultureInfo.InvariantCulture, _noPartialIdSql, semVerLevelSqlFilter));
}
var prereleaseFilter = string.Empty;
@@ -45,9 +58,9 @@ public Task> Execute(
prereleaseFilter = "AND p.IsPrerelease = {1}";
}
- var sql = string.Format(CultureInfo.InvariantCulture, _partialIdSqlFormat, prereleaseFilter);
+ var sql = string.Format(CultureInfo.InvariantCulture, _partialIdSqlFormat, semVerLevelSqlFilter, prereleaseFilter);
- return RunQuery(sql, partialId + "%", includePrerelease ?? false);
+ return RunSqlQuery(sql, partialId + "%", includePrerelease ?? false);
}
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs
index d138a736c0..0bf5bd9d78 100644
--- a/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs
+++ b/src/NuGetGallery/Queries/AutoCompleteDatabasePackageVersionsQuery.cs
@@ -14,9 +14,9 @@ public class AutoCompleteDatabasePackageVersionsQuery
private const string _sqlFormat = @"SELECT p.[Version]
FROM Packages p (NOLOCK)
JOIN PackageRegistrations pr (NOLOCK) on pr.[Key] = p.PackageRegistrationKey
-WHERE p.[SemVerLevelKey] IS NULL AND pr.ID = {{0}}
- {0}";
-
+WHERE {0} AND pr.ID = {{0}}
+ {1}";
+
public AutoCompleteDatabasePackageVersionsQuery(IEntitiesContext entities)
: base(entities)
{
@@ -24,20 +24,33 @@ public AutoCompleteDatabasePackageVersionsQuery(IEntitiesContext entities)
public Task> Execute(
string id,
- bool? includePrerelease = false)
+ bool? includePrerelease = false,
+ string semVerLevel = null)
{
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentNullException(nameof(id));
}
+ // Create SQL filter on SemVerLevel
+ // By default, we filter out SemVer v2.0.0 package versions.
+ var semVerLevelSqlFilter = "p.[SemVerLevelKey] IS NULL";
+ if (!string.IsNullOrEmpty(semVerLevel))
+ {
+ var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel);
+ if (semVerLevelKey == SemVerLevelKey.SemVer2)
+ {
+ semVerLevelSqlFilter = "p.[SemVerLevelKey] = " + SemVerLevelKey.SemVer2;
+ }
+ }
+
var prereleaseFilter = string.Empty;
if (!includePrerelease.HasValue || !includePrerelease.Value)
{
prereleaseFilter = "AND p.IsPrerelease = 0";
}
- return RunQuery(string.Format(CultureInfo.InvariantCulture, _sqlFormat, prereleaseFilter), id);
+ return RunSqlQuery(string.Format(CultureInfo.InvariantCulture, _sqlFormat, semVerLevelSqlFilter, prereleaseFilter), id);
}
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs b/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs
index 3d24af2ea2..4b1a54778f 100644
--- a/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs
+++ b/src/NuGetGallery/Queries/AutoCompleteDatabaseQuery.cs
@@ -23,7 +23,7 @@ public AutoCompleteDatabaseQuery(IEntitiesContext entities)
_dbContext = (DbContext)entities;
}
- public Task> RunQuery(string sql, params object[] sqlParameters)
+ public Task> RunSqlQuery(string sql, params object[] sqlParameters)
{
return Task.FromResult(_dbContext.Database.SqlQuery(sql, sqlParameters).AsEnumerable());
}
diff --git a/src/NuGetGallery/Queries/AutoCompleteServiceQuery.cs b/src/NuGetGallery/Queries/AutoCompleteServiceQuery.cs
index 13b8f623e8..706a4236dd 100644
--- a/src/NuGetGallery/Queries/AutoCompleteServiceQuery.cs
+++ b/src/NuGetGallery/Queries/AutoCompleteServiceQuery.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using NuGet.Services.Search.Client;
+using NuGet.Versioning;
using NuGetGallery.Configuration;
namespace NuGetGallery
@@ -30,9 +31,19 @@ public AutoCompleteServiceQuery(IAppConfiguration configuration)
_httpClient = new RetryingHttpClientWrapper(new HttpClient());
}
- public async Task> RunQuery(string queryString, bool? includePrerelease)
+ public async Task> RunServiceQuery(
+ string queryString,
+ bool? includePrerelease,
+ string semVerLevel = null)
{
queryString += $"&prerelease={includePrerelease ?? false}";
+
+ NuGetVersion semVerLevelVersion;
+ if (!string.IsNullOrEmpty(semVerLevel) && NuGetVersion.TryParse(semVerLevel, out semVerLevelVersion))
+ {
+ queryString += $"&semVerLevel={semVerLevel}";
+ }
+
var endpoints = await _serviceDiscoveryClient.GetEndpointsForResourceType(_autocompleteServiceResourceType);
endpoints = endpoints.Select(e => new Uri(e + "?" + queryString)).AsEnumerable();
diff --git a/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs b/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs
index b9c97bf283..d6207b96a8 100644
--- a/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs
+++ b/src/NuGetGallery/Queries/AutocompleteServicePackageIdsQuery.cs
@@ -18,11 +18,12 @@ public AutoCompleteServicePackageIdsQuery(IAppConfiguration configuration)
public async Task> Execute(
string partialId,
- bool? includePrerelease)
+ bool? includePrerelease,
+ string semVerLevel = null)
{
partialId = partialId ?? string.Empty;
- return await RunQuery("take=30&q=" + Uri.EscapeUriString(partialId), includePrerelease);
+ return await RunServiceQuery("take=30&q=" + Uri.EscapeUriString(partialId), includePrerelease, semVerLevel);
}
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs b/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs
index 87395ecdb3..671723aa31 100644
--- a/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs
+++ b/src/NuGetGallery/Queries/AutocompleteServicePackageVersionsQuery.cs
@@ -18,14 +18,15 @@ public AutoCompleteServicePackageVersionsQuery(IAppConfiguration configuration)
public async Task> Execute(
string id,
- bool? includePrerelease)
+ bool? includePrerelease,
+ string semVerLevel = null)
{
if (string.IsNullOrWhiteSpace(id))
{
throw new ArgumentNullException(nameof(id));
}
- return await RunQuery("id=" + Uri.EscapeUriString(id), includePrerelease);
+ return await RunServiceQuery("id=" + Uri.EscapeUriString(id), includePrerelease, semVerLevel);
}
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs b/src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs
index 5d59ebd9d0..f3903f6a6d 100644
--- a/src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs
+++ b/src/NuGetGallery/Queries/IAutoCompletePackageIdsQuery.cs
@@ -10,6 +10,7 @@ public interface IAutoCompletePackageIdsQuery
{
Task> Execute(
string partialId,
- bool? includePrerelease = false);
+ bool? includePrerelease = false,
+ string semVerLevel = null);
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs b/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs
index b5acb174fe..e6f52b0560 100644
--- a/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs
+++ b/src/NuGetGallery/Queries/IAutoCompletePackageVersionsQuery.cs
@@ -10,6 +10,7 @@ public interface IAutoCompletePackageVersionsQuery
{
Task> Execute(
string id,
- bool? includePrerelease = false);
+ bool? includePrerelease = false,
+ string semVerLevel = null);
}
}
\ No newline at end of file
diff --git a/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs b/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs
index 5c6ed4a39f..30256452bb 100644
--- a/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs
+++ b/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs
@@ -106,5 +106,115 @@ public void ReturnsUnknownForNonSemVer2CompliantDependenciesThatAreNotSemVer1Com
Assert.Equal(SemVerLevelKey.Unknown, key);
}
}
+
+ public class TheForSemVerLevelMethod
+ {
+ [Theory]
+ [InlineData("")]
+ [InlineData("this.is.not.a.version.string")]
+ [InlineData("1.0.0-alpha.01")] // no leading zeros in numeric identifiers
+ [InlineData("1.0.0")]
+ [InlineData("2.0.0-alpha")]
+ public void DefaultsToUnknownKeyWhenVersionStringIsInvalidOrLowerThanVersion200(string semVerLevel)
+ {
+ // Act
+ var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel);
+
+ // Assert
+ Assert.Equal(SemVerLevelKey.Unknown, semVerLevelKey);
+ }
+
+ [Theory]
+ [InlineData("3.0.0")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ public void ReturnsSemVer2KeyWhenVersionStringAtLeastVersion200(string semVerLevel)
+ {
+ // Act
+ var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel);
+
+ // Assert
+ Assert.Equal(SemVerLevelKey.SemVer2, semVerLevelKey);
+ }
+
+ [Fact]
+ public void DefaultsToUnknownKeyWhenVersionStringIsNull()
+ {
+ // Act
+ var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(null);
+
+ // Assert
+ Assert.Equal(SemVerLevelKey.Unknown, semVerLevelKey);
+ }
+ }
+
+ public class TheIsCompliantWithSemVerLevelMethod
+ {
+ [Theory]
+ // Versions higher than SemVer v2.0.0
+ [InlineData("3.0.0")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ // Versions lower than SemVer v2.0.0
+ [InlineData("2.0.0-alpha")] // no leading zeros in numeric identifiers
+ [InlineData("1.0.1")]
+ // Invalid/undefined versions
+ [InlineData(null)]
+ [InlineData("this.is.not.a.valid.version.string")]
+ [InlineData("2.0.0-alpha.01")] // no leading zeros in numeric identifiers
+ [InlineData("-2.0.1")]
+ public void UnknownKey_IsCompliantWithAnySemVerLevelString(string semVerLevel)
+ {
+ AssertPackageIsComplianceWithSemVerLevel(SemVerLevelKey.Unknown, semVerLevel, shouldBeCompliant: true);
+ }
+
+ [Theory]
+ // Versions higher than SemVer v2.0.0
+ [InlineData("3.0.0")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ public void SemVer2Key_IsCompliantWithSemVerLevel200OrHigher(string semVerLevel)
+ {
+ AssertPackageIsComplianceWithSemVerLevel(SemVerLevelKey.SemVer2, semVerLevel, shouldBeCompliant: true);
+ }
+
+ [Theory]
+ // Invalid versions
+ [InlineData("this.is.not.a.valid.version.string")]
+ [InlineData("2.0.0-alpha.01")] // no leading zeros in numeric identifiers
+ [InlineData("-2.0.1")]
+ public void SemVer2Key_IsNotCompliantWithInvalidVersionStrings(string semVerLevel)
+ {
+ AssertPackageIsComplianceWithSemVerLevel(SemVerLevelKey.SemVer2, semVerLevel, shouldBeCompliant: false);
+ }
+
+
+ [Theory]
+ // Versions lower than SemVer v2.0.0
+ [InlineData(null)]
+ [InlineData("2.0.0-alpha")] // no leading zeros in numeric identifiers
+ [InlineData("1.0.1")]
+ public void SemVer2Key_IsNotCompliantWithVersionStringLowerThanSemVer2(string semVerLevel)
+ {
+ AssertPackageIsComplianceWithSemVerLevel(SemVerLevelKey.SemVer2, semVerLevel, shouldBeCompliant: false);
+ }
+
+ [Fact]
+ public void SemVer2Key_IsNotCompliantWithUnknownSemVerLevel()
+ {
+ AssertPackageIsComplianceWithSemVerLevel(SemVerLevelKey.SemVer2, semVerLevel: null, shouldBeCompliant: false);
+ }
+
+ private static void AssertPackageIsComplianceWithSemVerLevel(int? packageSemVerLevelKey, string semVerLevel, bool shouldBeCompliant)
+ {
+ var package = new Package { SemVerLevelKey = packageSemVerLevelKey };
+ var compiledFunction = SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel).Compile();
+
+ Assert.Equal(shouldBeCompliant, compiledFunction(package));
+ }
+ }
}
}
\ No newline at end of file
diff --git a/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs
index 4ee610ef06..3d22e5af0a 100644
--- a/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs
+++ b/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs
@@ -22,7 +22,7 @@ public async Task Get_FiltersSemVerV2PackageVersions()
"/api/v1/Packages");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
@@ -47,7 +47,7 @@ public async Task FindPackagesById_FiltersSemVerV2PackageVersions()
$"/api/v1/FindPackagesById?id='{TestPackageId}'");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
@@ -60,7 +60,7 @@ public async Task Search_FiltersSemVerV2PackageVersions()
$"/api/v1/Search?searchTerm='{TestPackageId}'");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
@@ -82,7 +82,7 @@ protected override ODataV1FeedController CreateController(IEntityRepository resultSet)
+ private void AssertSemVer2PackagesFilteredFromResult(IEnumerable resultSet)
{
foreach (var feedPackage in resultSet)
{
diff --git a/tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs
index 339eed37ed..05f9495299 100644
--- a/tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs
+++ b/tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs
@@ -26,9 +26,26 @@ public async Task Get_FiltersSemVerV2PackageVersions()
$"/api/v2/curated-feed/{_curatedFeedName}/Packages");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task Get_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var resultSet = await GetCollection(
+ (controller, options) => controller.Get(options, _curatedFeedName, semVerLevel),
+ $"/api/v2/curated-feed/{_curatedFeedName}/Packages?semVerLevel={semVerLevel}");
+
+ // Assert
+ AssertSemVer2PackagesIncludedInResult(resultSet);
+ Assert.Equal(AllPackages.Count(), resultSet.Count);
+ }
[Fact]
public async Task GetCount_FiltersSemVerV2PackageVersions()
@@ -41,6 +58,22 @@ public async Task GetCount_FiltersSemVerV2PackageVersions()
// Assert
Assert.Equal(NonSemVer2Packages.Count, count);
}
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task GetCount_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var count = await GetInt(
+ (controller, options) => controller.GetCount(options, _curatedFeedName, semVerLevel),
+ $"/api/v2/curated-feed/{_curatedFeedName}/Packages/$count?semVerLevel={semVerLevel}");
+
+ // Assert
+ Assert.Equal(AllPackages.Count(), count);
+ }
[Fact]
public async Task FindPackagesById_FiltersSemVerV2PackageVersions()
@@ -51,35 +84,85 @@ public async Task FindPackagesById_FiltersSemVerV2PackageVersions()
$"/api/v2/curated-feed/{_curatedFeedName}/FindPackagesById?id='{TestPackageId}'");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task FindPackagesById_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var resultSet = await GetCollection(
+ async (controller, options) => await controller.FindPackagesById(options, _curatedFeedName, id: TestPackageId, semVerLevel: semVerLevel),
+ $"/api/v2/curated-feed/{_curatedFeedName}/FindPackagesById?id='{TestPackageId}'?semVerLevel={semVerLevel}");
+
+ // Assert
+ AssertSemVer2PackagesIncludedInResult(resultSet);
+ Assert.Equal(AllPackages.Count(), resultSet.Count);
+ }
+
[Fact]
public async Task Search_FiltersSemVerV2PackageVersions()
{
// Act
var resultSet = await GetCollection(
- async (controller, options) => await controller.Search(options, _curatedFeedName, TestPackageId),
+ async (controller, options) => await controller.Search(options, _curatedFeedName, searchTerm: TestPackageId),
$"/api/v2/curated-feed/{_curatedFeedName}/Search?searchTerm='{TestPackageId}'");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task Search_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var resultSet = await GetCollection(
+ async (controller, options) => await controller.Search(options, _curatedFeedName, searchTerm: TestPackageId, semVerLevel: semVerLevel),
+ $"/api/v2/curated-feed/{_curatedFeedName}/Search?searchTerm='{TestPackageId}'?semVerLevel={semVerLevel}");
+
+ // Assert
+ AssertSemVer2PackagesIncludedInResult(resultSet);
+ Assert.Equal(AllPackages.Count(), resultSet.Count);
+ }
[Fact]
public async Task SearchCount_FiltersSemVerV2PackageVersions()
{
// Act
var searchCount = await GetInt(
- async (controller, options) => await controller.SearchCount(options, _curatedFeedName, TestPackageId),
+ async (controller, options) => await controller.SearchCount(options, _curatedFeedName, searchTerm: TestPackageId),
$"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'");
// Assert
Assert.Equal(NonSemVer2Packages.Count, searchCount);
}
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task SearchCount_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var searchCount = await GetInt(
+ async (controller, options) => await controller.SearchCount(options, _curatedFeedName, searchTerm: TestPackageId, semVerLevel: semVerLevel),
+ $"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'&semVerLevel={semVerLevel}");
+
+ // Assert
+ Assert.Equal(AllPackages.Count(), searchCount);
+ }
+
protected override ODataV2CuratedFeedController CreateController(
IEntityRepository packagesRepository,
IGalleryConfigurationService configurationService,
@@ -115,7 +198,7 @@ private static IDbSet GetQueryableMockDbSet(params T[] sourceList) where T
return dbSet.Object;
}
- private void AssertResultCorrect(IEnumerable resultSet)
+ private void AssertSemVer2PackagesFilteredFromResult(IEnumerable resultSet)
{
foreach (var feedPackage in resultSet)
{
@@ -128,5 +211,28 @@ private void AssertResultCorrect(IEnumerable resultSet)
string.Equals(p.PackageRegistration.Id, feedPackage.Id)));
}
}
+
+ private void AssertSemVer2PackagesIncludedInResult(IReadOnlyCollection resultSet)
+ {
+ foreach (var package in SemVer2Packages)
+ {
+ // Assert all of the SemVer2 packages are included in the result.
+ // Whilst at it, also check the NormalizedVersion on the OData feed.
+ Assert.Single(resultSet.Where(feedPackage =>
+ string.Equals(feedPackage.Version, package.Version)
+ && string.Equals(feedPackage.NormalizedVersion, package.NormalizedVersion)
+ && string.Equals(feedPackage.Id, package.PackageRegistration.Id)));
+ }
+
+ foreach (var package in NonSemVer2Packages)
+ {
+ // Assert all of the non-SemVer2 packages are included in the result.
+ // Whilst at it, also check the NormalizedVersion on the OData feed.
+ Assert.Single(resultSet.Where(feedPackage =>
+ string.Equals(feedPackage.Version, package.Version)
+ && string.Equals(feedPackage.NormalizedVersion, package.NormalizedVersion)
+ && string.Equals(feedPackage.Id, package.PackageRegistration.Id)));
+ }
+ }
}
}
\ No newline at end of file
diff --git a/tests/NuGetGallery.Facts/Controllers/ODataV2FeedControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ODataV2FeedControllerFacts.cs
index 2498c1f610..4131e5a67f 100644
--- a/tests/NuGetGallery.Facts/Controllers/ODataV2FeedControllerFacts.cs
+++ b/tests/NuGetGallery.Facts/Controllers/ODataV2FeedControllerFacts.cs
@@ -15,7 +15,7 @@ public class ODataV2FeedControllerFacts
: ODataFeedControllerFactsBase
{
[Fact]
- public async Task Get_FiltersSemVerV2PackageVersions()
+ public async Task Get_FiltersSemVerV2PackageVersionsByDefault()
{
// Act
var resultSet = await GetCollection(
@@ -23,12 +23,29 @@ public async Task Get_FiltersSemVerV2PackageVersions()
"/api/v2/Packages");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task Get_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var resultSet = await GetCollection(
+ (controller, options) => controller.Get(options, semVerLevel),
+ $"/api/v2/Packages?semVerLevel={semVerLevel}");
+
+ // Assert
+ AssertSemVer2PackagesIncludedInResult(resultSet);
+ Assert.Equal(AllPackages.Count(), resultSet.Count);
+ }
+
[Fact]
- public async Task GetCount_FiltersSemVerV2PackageVersions()
+ public async Task GetCount_FiltersSemVerV2PackageVersionsByDefault()
{
// Act
var count = await GetInt(
@@ -38,9 +55,25 @@ public async Task GetCount_FiltersSemVerV2PackageVersions()
// Assert
Assert.Equal(NonSemVer2Packages.Count, count);
}
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task GetCount_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var count = await GetInt(
+ (controller, options) => controller.GetCount(options, semVerLevel),
+ $"/api/v2/Packages/$count?semVerLevel={semVerLevel}");
+
+ // Assert
+ Assert.Equal(AllPackages.Count(), count);
+ }
[Fact]
- public async Task FindPackagesById_FiltersSemVerV2PackageVersions()
+ public async Task FindPackagesById_FiltersSemVerV2PackageVersionsByDefault()
{
// Act
var resultSet = await GetCollection(
@@ -48,37 +81,87 @@ public async Task FindPackagesById_FiltersSemVerV2PackageVersions()
$"/api/v2/FindPackagesById?id='{TestPackageId}'");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task FindPackagesById_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var resultSet = await GetCollection(
+ (controller, options) => controller.FindPackagesById(options, id: TestPackageId, semVerLevel: semVerLevel),
+ $"/api/v2/FindPackagesById?id='{TestPackageId}'?semVerLevel={semVerLevel}");
+
+ // Assert
+ AssertSemVer2PackagesIncludedInResult(resultSet);
+ Assert.Equal(AllPackages.Count(), resultSet.Count);
+ }
+
[Fact]
- public async Task Search_FiltersSemVerV2PackageVersions()
+ public async Task Search_FiltersSemVerV2PackageVersionsByDefault()
{
// Act
var resultSet = await GetCollection(
- async (controller, options) => await controller.Search(options, TestPackageId),
+ async (controller, options) => await controller.Search(options, searchTerm: TestPackageId),
$"/api/v2/Search?searchTerm='{TestPackageId}'");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
Assert.Equal(NonSemVer2Packages.Count, resultSet.Count);
}
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task Search_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var resultSet = await GetCollection(
+ (controller, options) => controller.Search(options, searchTerm: TestPackageId, semVerLevel: semVerLevel),
+ $"/api/v2/Search?searchTerm='{TestPackageId}'?semVerLevel={semVerLevel}");
+
+ // Assert
+ AssertSemVer2PackagesIncludedInResult(resultSet);
+ Assert.Equal(AllPackages.Count(), resultSet.Count);
+ }
[Fact]
- public async Task SearchCount_FiltersSemVerV2PackageVersions()
+ public async Task SearchCount_FiltersSemVerV2PackageVersionsByDefault()
{
// Act
var searchCount = await GetInt(
- async (controller, options) => await controller.SearchCount(options, TestPackageId),
+ async (controller, options) => await controller.SearchCount(options, searchTerm: TestPackageId),
$"/api/v2/Search/$count?searchTerm='{TestPackageId}'");
// Assert
Assert.Equal(NonSemVer2Packages.Count, searchCount);
}
-
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task SearchCount_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Act
+ var searchCount = await GetInt(
+ async (controller, options) => await controller.SearchCount(options, searchTerm: TestPackageId, semVerLevel: semVerLevel),
+ $"/api/v2/Search/$count?searchTerm='{TestPackageId}'&semVerLevel={semVerLevel}");
+
+ // Assert
+ Assert.Equal(AllPackages.Count(), searchCount);
+ }
+
[Fact]
- public async Task GetUpdates_FiltersSemVerV2PackageVersions()
+ public async Task GetUpdates_FiltersSemVerV2PackageVersionsByDefault()
{
// Arrange
const string currentVersionString = "1.0.0";
@@ -91,12 +174,53 @@ public async Task GetUpdates_FiltersSemVerV2PackageVersions()
$"/api/v2/GetUpdates()?packageIds='{TestPackageId}'&versions='{currentVersionString}'&includePrerelease=true&includeAllVersions=true");
// Assert
- AssertResultCorrect(resultSet);
+ AssertSemVer2PackagesFilteredFromResult(resultSet);
+ Assert.Equal(expected.Count(), resultSet.Count);
+ }
+
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task GetUpdates_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Arrange
+ const string currentVersionString = "1.0.0";
+ var currentVersion = NuGetVersion.Parse(currentVersionString);
+ var expected = AllPackages.Where(p => NuGetVersion.Parse(p.Version) > currentVersion);
+
+ // Act
+ var resultSet = await GetCollection(
+ (controller, options) => controller.GetUpdates(options, TestPackageId, currentVersionString, includePrerelease: true, includeAllVersions: true, semVerLevel: semVerLevel),
+ $"/api/v2/GetUpdates()?packageIds='{TestPackageId}'&versions='{currentVersionString}'&includePrerelease=true&includeAllVersions=true&semVerLevel={semVerLevel}");
+
+ // Assert
+ foreach (var package in SemVer2Packages.Where(p => NuGetVersion.Parse(p.Version) > currentVersion))
+ {
+ // Assert all of the SemVer2 packages are included in the result.
+ // Whilst at it, also check the NormalizedVersion on the OData feed.
+ Assert.Single(resultSet.Where(feedPackage =>
+ string.Equals(feedPackage.Version, package.Version)
+ && string.Equals(feedPackage.NormalizedVersion, package.NormalizedVersion)
+ && string.Equals(feedPackage.Id, package.PackageRegistration.Id)));
+ }
+
+ foreach (var package in NonSemVer2Packages.Where(p => NuGetVersion.Parse(p.Version) > currentVersion))
+ {
+ // Assert all of the non-SemVer2 packages are included in the result.
+ // Whilst at it, also check the NormalizedVersion on the OData feed.
+ Assert.Single(resultSet.Where(feedPackage =>
+ string.Equals(feedPackage.Version, package.Version)
+ && string.Equals(feedPackage.NormalizedVersion, package.NormalizedVersion)
+ && string.Equals(feedPackage.Id, package.PackageRegistration.Id)));
+ }
+
Assert.Equal(expected.Count(), resultSet.Count);
}
[Fact]
- public async Task GetUpdatesCount_FiltersSemVerV2PackageVersions()
+ public async Task GetUpdatesCount_FiltersSemVerV2PackageVersionsByDefault()
{
// Arrange
const string currentVersionString = "1.0.0";
@@ -112,6 +236,33 @@ public async Task GetUpdatesCount_FiltersSemVerV2PackageVersions()
Assert.Equal(expected.Count(), updatesCount);
}
+ [Theory]
+ [InlineData("2.0.0")]
+ [InlineData("2.0.1")]
+ [InlineData("3.0.0-alpha")]
+ [InlineData("3.0.0")]
+ public async Task GetUpdatesCount_IncludesSemVerV2PackageVersionsWhenSemVerLevel2OrHigher(string semVerLevel)
+ {
+ // Arrange
+ const string currentVersionString = "1.0.0";
+ var currentVersion = NuGetVersion.Parse(currentVersionString);
+ var expected = AllPackages.Where(p => NuGetVersion.Parse(p.Version) > currentVersion);
+
+ // Act
+ var searchCount = await GetInt(
+ (controller, options) => controller.GetUpdatesCount(
+ options,
+ packageIds: TestPackageId,
+ versions: currentVersionString,
+ includePrerelease: true,
+ includeAllVersions: true,
+ semVerLevel: semVerLevel),
+ $"/api/v2/GetUpdates()?packageIds='{TestPackageId}'&versions='{currentVersionString}'&includePrerelease=true&includeAllVersions=true&semVerLevel={semVerLevel}");
+
+ // Assert
+ Assert.Equal(expected.Count(), searchCount);
+ }
+
protected override ODataV2FeedController CreateController(
IEntityRepository packagesRepository,
IGalleryConfigurationService configurationService,
@@ -120,7 +271,7 @@ protected override ODataV2FeedController CreateController(
return new ODataV2FeedController(packagesRepository, configurationService, searchService);
}
- private void AssertResultCorrect(IEnumerable resultSet)
+ private void AssertSemVer2PackagesFilteredFromResult(IEnumerable resultSet)
{
foreach (var feedPackage in resultSet)
{
@@ -133,5 +284,28 @@ private void AssertResultCorrect(IEnumerable resultSet)
string.Equals(p.PackageRegistration.Id, feedPackage.Id)));
}
}
+
+ private void AssertSemVer2PackagesIncludedInResult(IReadOnlyCollection resultSet)
+ {
+ foreach (var package in SemVer2Packages)
+ {
+ // Assert all of the SemVer2 packages are included in the result.
+ // Whilst at it, also check the NormalizedVersion on the OData feed.
+ Assert.Single(resultSet.Where(feedPackage =>
+ string.Equals(feedPackage.Version, package.Version)
+ && string.Equals(feedPackage.NormalizedVersion, package.NormalizedVersion)
+ && string.Equals(feedPackage.Id, package.PackageRegistration.Id)));
+ }
+
+ foreach (var package in NonSemVer2Packages)
+ {
+ // Assert all of the non-SemVer2 packages are included in the result.
+ // Whilst at it, also check the NormalizedVersion on the OData feed.
+ Assert.Single(resultSet.Where(feedPackage =>
+ string.Equals(feedPackage.Version, package.Version)
+ && string.Equals(feedPackage.NormalizedVersion, package.NormalizedVersion)
+ && string.Equals(feedPackage.Id, package.PackageRegistration.Id)));
+ }
+ }
}
}
\ No newline at end of file
diff --git a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs
index bf60fa44d6..536149c8a7 100644
--- a/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs
+++ b/tests/NuGetGallery.Facts/Services/FeedServiceFacts.cs
@@ -635,7 +635,9 @@ public async Task V2FeedPackagesByIdAndVersionReturnsPackage(string expectedId,
searchService.Setup(s => s.ContainsAllVersions).Returns(false);
var v2Service = new TestableV2Feed(repo.Object, configuration.Object, searchService.Object);
- v2Service.Request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:8081/api/v2/Packages(Id='" + expectedId + "', Version='" + expectedVersion + "')");
+ v2Service.Request = new HttpRequestMessage(
+ HttpMethod.Get,
+ $"https://localhost:8081/api/v2/Packages(Id=\'{expectedId}\', Version=\'{expectedVersion}\')");
// Act
var result = (await v2Service.Get(new ODataQueryOptions(new ODataQueryContext(NuGetODataV2FeedConfig.GetEdmModel(), typeof(V2FeedPackage)), v2Service.Request), expectedId, expectedVersion))
diff --git a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs
index 96f2dce4b6..11835fd7a8 100644
--- a/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs
+++ b/tests/NuGetGallery.Facts/TestUtils/Infrastructure/FeedServiceHelpers.cs
@@ -45,6 +45,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = fooPackage,
Version = "1.0.0",
+ NormalizedVersion = "1.0.0",
IsPrerelease = false,
Listed = true,
Authors = new [] { new PackageAuthor { Name = "Test "} },
@@ -57,6 +58,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = fooPackage,
Version = "1.0.1-a",
+ NormalizedVersion = "1.0.1-a",
IsPrerelease = true,
Listed = true,
Authors = new [] { new PackageAuthor { Name = "Test "} },
@@ -69,6 +71,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = barPackage,
Version = "1.0.0",
+ NormalizedVersion = "1.0.0",
IsPrerelease = false,
Listed = true,
Authors = new [] { new PackageAuthor { Name = "Test "} },
@@ -81,6 +84,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = barPackage,
Version = "2.0.0",
+ NormalizedVersion = "2.0.0",
IsPrerelease = false,
Listed = true,
Authors = new [] { new PackageAuthor { Name = "Test "} },
@@ -93,6 +97,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = barPackage,
Version = "2.0.1-a",
+ NormalizedVersion = "2.0.1-a",
IsPrerelease = true,
Listed = true,
Authors = new [] { new PackageAuthor { Name = "Test "} },
@@ -105,6 +110,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = barPackage,
Version = "2.0.1-b",
+ NormalizedVersion = "2.0.1-b",
IsPrerelease = true,
Listed = false,
Authors = new [] { new PackageAuthor { Name = "Test "} },
@@ -117,6 +123,7 @@ public static Mock> SetupTestPackageRepository()
{
PackageRegistration = bazPackage,
Version = "1.0.0",
+ NormalizedVersion = "1.0.0",
IsPrerelease = false,
Listed = false,
Deleted = true, // plot twist: this package is a soft-deleted one