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