diff --git a/src/NuGet.Services.Search.Client/Client/SearchClient.cs b/src/NuGet.Services.Search.Client/Client/SearchClient.cs index 178fba219a..1f5f9ffaf0 100644 --- a/src/NuGet.Services.Search.Client/Client/SearchClient.cs +++ b/src/NuGet.Services.Search.Client/Client/SearchClient.cs @@ -83,7 +83,8 @@ public async Task> Search( bool countOnly = false, bool explain = false, bool getAllVersions = false, - string supportedFramework = null) + string supportedFramework = null, + string semVerLevel = null) { IDictionary nameValue = new Dictionary(); nameValue.Add("q", query); @@ -91,6 +92,11 @@ public async Task> Search( nameValue.Add("take", take.ToString()); nameValue.Add("sortBy", SortNames[sortBy]); + if (!String.IsNullOrEmpty(semVerLevel)) + { + nameValue.Add("semVerLevel", semVerLevel); + } + if (!String.IsNullOrEmpty(supportedFramework)) { nameValue.Add("supportedFramework", supportedFramework); diff --git a/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs b/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs index 2df9d40619..1a1e225a05 100644 --- a/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs +++ b/src/NuGetGallery.Core/Auditing/PackageAuditRecord.cs @@ -49,7 +49,7 @@ public PackageAuditRecord(Package package, AuditedPackageAction action) public override string GetPath() { - return $"{Id}/{NuGetVersionNormalizer.Normalize(Version)}" + return $"{Id}/{NuGetVersionFormatter.Normalize(Version)}" .ToLowerInvariant(); } } diff --git a/src/NuGetGallery.Core/CoreStrings.Designer.cs b/src/NuGetGallery.Core/CoreStrings.Designer.cs index 3b5f610a4b..001cf3caa2 100644 --- a/src/NuGetGallery.Core/CoreStrings.Designer.cs +++ b/src/NuGetGallery.Core/CoreStrings.Designer.cs @@ -123,6 +123,15 @@ public static string Manifest_InvalidDependency { } } + /// + /// Looks up a localized string similar to The package manifest contains an invalid Dependency Version: '{0}'. + /// + public static string Manifest_InvalidDependencyVersion { + get { + return ResourceManager.GetString("Manifest_InvalidDependencyVersion", resourceCulture); + } + } + /// /// Looks up a localized string similar to The package manifest contains an invalid ID: '{0}'. /// @@ -168,15 +177,6 @@ public static string Manifest_InvalidVersion { } } - /// - /// Looks up a localized string similar to The version '{0}' is not supported. The NuGet Gallery currently does not currently support Semantic Version 2.0 as it would break older NuGet clients.. - /// - public static string Manifest_InvalidVersionSemVer200 { - get { - return ResourceManager.GetString("Manifest_InvalidVersionSemVer200", resourceCulture); - } - } - /// /// Looks up a localized string similar to The package manifest is missing the Id field. /// diff --git a/src/NuGetGallery.Core/CoreStrings.resx b/src/NuGetGallery.Core/CoreStrings.resx index 0fc99a2625..e865c11acd 100644 --- a/src/NuGetGallery.Core/CoreStrings.resx +++ b/src/NuGetGallery.Core/CoreStrings.resx @@ -168,9 +168,6 @@ The target framework {0} is not supported. - - The version '{0}' is not supported. The NuGet Gallery currently does not currently support Semantic Version 2.0 as it would break older NuGet clients. - (404) Error - Not Found @@ -180,4 +177,7 @@ Negative indexes are invalid. + + The package manifest contains an invalid Dependency Version: '{0}' + \ No newline at end of file diff --git a/src/NuGetGallery.Core/Entities/Package.cs b/src/NuGetGallery.Core/Entities/Package.cs index e5ca80387b..1e11f720e4 100644 --- a/src/NuGetGallery.Core/Entities/Package.cs +++ b/src/NuGetGallery.Core/Entities/Package.cs @@ -80,6 +80,9 @@ public Package() public bool IsLatest { get; set; } public bool IsLatestStable { get; set; } + public bool IsLatestSemVer2 { get; set; } + public bool IsLatestStableSemVer2 { get; set; } + /// /// This is when the Package Entity was last touched (so caches can notice changes). In UTC. /// diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index 30daf1e90a..b54fe187c1 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -85,19 +85,19 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\..\packages\NuGet.Common.4.3.0-preview1-2507\lib\net45\NuGet.Common.dll + ..\..\packages\NuGet.Common.4.3.0-preview1-2524\lib\net45\NuGet.Common.dll - ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2507\lib\net45\NuGet.Frameworks.dll + ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2524\lib\net45\NuGet.Frameworks.dll - ..\..\packages\NuGet.Packaging.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.dll + ..\..\packages\NuGet.Packaging.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.dll - ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.Core.dll + ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.Core.dll - ..\..\packages\NuGet.Versioning.4.3.0-preview1-2507\lib\net45\NuGet.Versioning.dll + ..\..\packages\NuGet.Versioning.4.3.0-preview1-2524\lib\net45\NuGet.Versioning.dll diff --git a/src/NuGetGallery.Core/NuGetVersionExtensions.cs b/src/NuGetGallery.Core/NuGetVersionExtensions.cs index 5b83a506a8..ab66e7e3de 100644 --- a/src/NuGetGallery.Core/NuGetVersionExtensions.cs +++ b/src/NuGetGallery.Core/NuGetVersionExtensions.cs @@ -7,7 +7,7 @@ namespace NuGetGallery { - public static class NuGetVersionNormalizer + public static class NuGetVersionFormatter { public static string Normalize(string version) { @@ -19,16 +19,34 @@ public static string Normalize(string version) return parsed.ToNormalizedString(); } + + public static string ToFullStringOrFallback(string version, string fallback = "") + { + NuGetVersion nugetVersion; + if (NuGetVersion.TryParse(version, out nugetVersion)) + { + return nugetVersion.ToFullString(); + } + else + { + return fallback; + } + } } public static class NuGetVersionExtensions { - private const RegexOptions Flags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; - private static readonly Regex SemanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", Flags); + private const RegexOptions SemanticVersionRegexFlags = RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture; + private static readonly Regex SemanticVersionRegex = new Regex(@"^(?\d+(\s*\.\s*\d+){0,3})(?-[a-z][0-9a-z-]*)?$", SemanticVersionRegexFlags); public static string ToNormalizedStringSafe(this NuGetVersion self) { - return self != null ? self.ToNormalizedString() : String.Empty; + return self != null ? self.ToNormalizedString() : string.Empty; + } + + public static string ToFullStringSafe(this NuGetVersion self) + { + return self != null ? self.ToFullString() : string.Empty; } public static bool IsValidVersionForLegacyClients(this NuGetVersion self) diff --git a/src/NuGetGallery.Core/Packaging/ManifestValidator.cs b/src/NuGetGallery.Core/Packaging/ManifestValidator.cs index c17b1d2613..024bdca0fd 100644 --- a/src/NuGetGallery.Core/Packaging/ManifestValidator.cs +++ b/src/NuGetGallery.Core/Packaging/ManifestValidator.cs @@ -28,7 +28,7 @@ public static IEnumerable Validate(Stream nuspecStream, out Nu catch (Exception ex) { nuspecReader = null; - return new [] { new ValidationResult(ex.Message) }; + return new[] { new ValidationResult(ex.Message) }; } return Enumerable.Empty(); @@ -59,7 +59,7 @@ private static IEnumerable ValidateCore(PackageMetadata packag // Check and validate URL properties foreach (var result in CheckUrls( packageMetadata.GetValueFromMetadata("IconUrl"), - packageMetadata.GetValueFromMetadata("ProjectUrl"), + packageMetadata.GetValueFromMetadata("ProjectUrl"), packageMetadata.GetValueFromMetadata("LicenseUrl"))) { yield return result; @@ -76,7 +76,7 @@ private static IEnumerable ValidateCore(PackageMetadata packag version)); } - var versionValidationResult = ValidateVersion(packageMetadata.Version); + var versionValidationResult = ValidateVersionForLegacyClients(packageMetadata.Version); if (versionValidationResult != null) { yield return versionValidationResult; @@ -143,17 +143,19 @@ private static IEnumerable ValidateCore(PackageMetadata packag // Versions if (dependency.VersionRange.MinVersion != null) { - var versionRangeValidationResult = ValidateVersion(dependency.VersionRange.MinVersion); + var versionRangeValidationResult = + ValidateDependencyVersion(dependency.VersionRange.MinVersion); if (versionRangeValidationResult != null) { yield return versionRangeValidationResult; } } - if (dependency.VersionRange.MaxVersion != null + if (dependency.VersionRange.MaxVersion != null && dependency.VersionRange.MaxVersion != dependency.VersionRange.MinVersion) { - var versionRangeValidationResult = ValidateVersion(dependency.VersionRange.MaxVersion); + var versionRangeValidationResult = + ValidateDependencyVersion(dependency.VersionRange.MaxVersion); if (versionRangeValidationResult != null) { yield return versionRangeValidationResult; @@ -164,24 +166,37 @@ private static IEnumerable ValidateCore(PackageMetadata packag } } - private static ValidationResult ValidateVersion(NuGetVersion version) + /// + /// Checks whether the provided version is consumable by legacy 2.x clients, + /// which do not support a `.` in release labels, or release labels starting with numeric characters. + /// See also https://github.com/NuGet/NuGetGallery/issues/3226. + /// + /// The to check for 2.x client compatibility. + /// Returns a when non-compliant; otherwise null. + private static ValidationResult ValidateVersionForLegacyClients(NuGetVersion version) { - if (version.IsSemVer2) + if (!version.IsSemVer2 && !version.IsValidVersionForLegacyClients()) { return new ValidationResult(string.Format( CultureInfo.CurrentCulture, - CoreStrings.Manifest_InvalidVersionSemVer200, - version.ToFullString())); + CoreStrings.Manifest_InvalidVersion, + version)); } - else if (!version.IsValidVersionForLegacyClients()) + + return null; + } + + private static ValidationResult ValidateDependencyVersion(NuGetVersion version) + { + if (version.HasMetadata) { return new ValidationResult(string.Format( CultureInfo.CurrentCulture, - CoreStrings.Manifest_InvalidVersion, - version)); + CoreStrings.Manifest_InvalidDependencyVersion, + version.ToFullString())); } - return null; + return ValidateVersionForLegacyClients(version); } private static IEnumerable CheckUrls(params string[] urls) diff --git a/src/NuGetGallery.Core/SemVerLevelKey.cs b/src/NuGetGallery.Core/SemVerLevelKey.cs index 33ba34eef7..8010e0f76c 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,9 @@ namespace NuGetGallery /// public static class SemVerLevelKey { + public static readonly string SemVerLevel2 = "2.0.0"; + private static readonly NuGetVersion _semVer2Version = NuGetVersion.Parse(SemVerLevel2); + /// /// This could either indicate being SemVer1-compliant, or non-SemVer-compliant at all (e.g. System.Versioning pattern). /// @@ -31,7 +35,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) @@ -53,17 +57,66 @@ public static class SemVerLevelKey // Check the package dependencies for SemVer-compliance. // As soon as a SemVer2-compliant dependency version is found that is not SemVer1-compliant, // this package in itself is to be identified as to have SemVerLevelKey.SemVer2. - var dependencyVersionRange = VersionRange.Parse(dependency.VersionSpec); - - if ((dependencyVersionRange.MinVersion != null && dependencyVersionRange.MinVersion.IsSemVer2) - || (dependencyVersionRange.MaxVersion != null && dependencyVersionRange.MaxVersion.IsSemVer2)) + VersionRange dependencyVersionRange; + if (dependency.VersionSpec != null && VersionRange.TryParse(dependency.VersionSpec, out dependencyVersionRange)) { - return SemVer2; + if ((dependencyVersionRange.MinVersion != null && dependencyVersionRange.MinVersion.IsSemVer2) + || (dependencyVersionRange.MaxVersion != null && dependencyVersionRange.MaxVersion.IsSemVer2)) + { + return SemVer2; + } } } } 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.Core/packages.config b/src/NuGetGallery.Core/packages.config index 560cd1ddf8..6a778ed237 100644 --- a/src/NuGetGallery.Core/packages.config +++ b/src/NuGetGallery.Core/packages.config @@ -9,11 +9,11 @@ - - - - - + + + + + \ 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/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs index b98594ba9b..50c7fa07c3 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs @@ -63,7 +63,7 @@ public virtual ActionResult Search(string query) } else if (splitQueryPart.Length == 2) { - var resultingPackage = _packageService.FindPackageByIdAndVersion(splitQueryPart[0].Trim(), splitQueryPart[1].Trim(), true); + var resultingPackage = _packageService.FindPackageByIdAndVersionStrict(splitQueryPart[0].Trim(), splitQueryPart[1].Trim()); if (resultingPackage != null) { results.Add(CreateDeleteSearchResult(resultingPackage)); diff --git a/src/NuGetGallery/Controllers/ApiController.cs b/src/NuGetGallery/Controllers/ApiController.cs index 75b6648294..18ef5b4075 100644 --- a/src/NuGetGallery/Controllers/ApiController.cs +++ b/src/NuGetGallery/Controllers/ApiController.cs @@ -127,7 +127,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"); } @@ -142,15 +142,20 @@ public virtual async Task GetPackage(string id, string version) } // Normalize the version - version = NuGetVersionNormalizer.Normalize(version); + version = NuGetVersionFormatter.Normalize(version); } else { - // if version is null, get the latest version from the database. + // If version is null, get the latest version from the database. // This ensures that on package restore scenario where version will be non null, we don't hit the database. try { - var package = PackageService.FindPackageByIdAndVersion(id, version, allowPrerelease: false); + var package = PackageService.FindPackageByIdAndVersion( + id, + version, + SemVerLevelKey.SemVer2, + allowPrerelease: false); + if (package == null) { return new HttpStatusCodeWithBodyResult(HttpStatusCode.NotFound, String.Format(CultureInfo.CurrentCulture, Strings.PackageWithIdAndVersionNotFound, id, version)); @@ -261,7 +266,7 @@ public async virtual Task VerifyPackageKeyAsync(string id, string private async Task VerifyPackageKeyInternalAsync(User user, Credential credential, string id, string version) { // Verify that the user has permission to push for the specific Id \ version combination. - var package = PackageService.FindPackageByIdAndVersion(id, version); + var package = PackageService.FindPackageByIdAndVersion(id, version, semVerLevelKey: SemVerLevelKey.SemVer2); if (package == null) { return new HttpStatusCodeWithBodyResult( @@ -495,8 +500,8 @@ await AuditingService.SaveAuditRecordAsync( // Notify user of push MessageService.SendPackageAddedNotice(package, - Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), - Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), + Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.NormalizedVersion }, protocol: Request.Url.Scheme), + Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.NormalizedVersion }, protocol: Request.Url.Scheme), Url.Action("Account", "Users", routeValues: null, protocol: Request.Url.Scheme)); TelemetryService.TrackPackagePushEvent(package, user, User.Identity); @@ -544,7 +549,7 @@ private static ActionResult BadRequestForExceptionMessage(Exception ex) [ActionName("DeletePackageApi")] public virtual async Task DeletePackage(string id, string version) { - var package = PackageService.FindPackageByIdAndVersion(id, version); + var package = PackageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return new HttpStatusCodeWithBodyResult( @@ -577,7 +582,7 @@ public virtual async Task DeletePackage(string id, string version) [ActionName("PublishPackageApi")] public virtual async Task PublishPackage(string id, string version) { - var package = PackageService.FindPackageByIdAndVersion(id, version); + var package = PackageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return new HttpStatusCodeWithBodyResult( @@ -646,24 +651,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/CuratedFeedsController.cs b/src/NuGetGallery/Controllers/CuratedFeedsController.cs index 07d469df51..d7654caccf 100644 --- a/src/NuGetGallery/Controllers/CuratedFeedsController.cs +++ b/src/NuGetGallery/Controllers/CuratedFeedsController.cs @@ -62,9 +62,15 @@ public virtual async Task ListPackages(string curatedFeedName, str page = 1; } - q = (q ?? "").Trim(); + q = (q ?? string.Empty).Trim(); + + var searchFilter = SearchAdaptor.GetSearchFilter( + q, + page, + sortOrder: null, + context: SearchFilter.UISearchContext, + semVerLevel: SemVerLevelKey.SemVerLevel2); - var searchFilter = SearchAdaptor.GetSearchFilter(q, page, sortOrder: null, context: SearchFilter.UISearchContext); searchFilter.CuratedFeed = CuratedFeedService.GetFeedByName(curatedFeedName, includePackages: false); if (searchFilter.CuratedFeed == null) { @@ -91,7 +97,7 @@ public virtual async Task ListPackages(string curatedFeedName, str ViewBag.SearchTerm = q; - return View("ListPackages", viewModel); + return View("ListPackages", viewModel); } } } diff --git a/src/NuGetGallery/Controllers/ODataV1FeedController.cs b/src/NuGetGallery/Controllers/ODataV1FeedController.cs index 0fe79dd513..8fba345d97 100644 --- a/src/NuGetGallery/Controllers/ODataV1FeedController.cs +++ b/src/NuGetGallery/Controllers/ODataV1FeedController.cs @@ -101,7 +101,13 @@ private async Task GetCore(ODataQueryOptions o try { var searchAdaptorResult = await SearchAdaptor.FindByIdAndVersionCore( - _searchService, GetTraditionalHttpContext().Request, packages, id, version, curatedFeed: null); + _searchService, + GetTraditionalHttpContext().Request, + packages, + id, + version, + curatedFeed: null, + semVerLevel: null); // If intercepted, create a paged queryresult if (searchAdaptorResult.ResultsAreProvidedBySearchService) @@ -190,7 +196,14 @@ public async Task Search( // todo: search hijack should take queryOptions instead of manually parsing query options var searchAdaptorResult = await SearchAdaptor.SearchCore( - _searchService, GetTraditionalHttpContext().Request, packages, searchTerm, targetFramework, false, curatedFeed: null); + _searchService, + GetTraditionalHttpContext().Request, + packages, + searchTerm, + targetFramework, + false, + curatedFeed: null, + semVerLevel: null); // Packages provided by search service (even when not hijacked) var query = searchAdaptorResult.Packages; diff --git a/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs b/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs index 4a6659ec7e..a613140515 100644 --- a/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs +++ b/src/NuGetGallery/Controllers/ODataV2CuratedFeedController.cs @@ -40,31 +40,42 @@ 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)) { return NotFound(); } + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + var queryable = _curatedFeedService.GetPackages(curatedFeedName) - .Where(p => p.SemVerLevelKey == SemVerLevelKey.Unknown) - .ToV2FeedPackageQuery(_configurationService.GetSiteRoot(UseHttps()), _configurationService.Features.FriendlyLicenses) + .Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel)) + .ToV2FeedPackageQuery( + _configurationService.GetSiteRoot(UseHttps()), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey) .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,28 +83,40 @@ 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: SemVerLevelKey.SemVerLevel2); 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)) { + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + var emptyResult = Enumerable.Empty().AsQueryable() - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses, semVerLevelKey); return QueryResult(options, emptyResult, MaxPageSize); } - return await GetCore(options, curatedFeedName, id, version: null, return404NotFoundWhenNoResults: false); + return await GetCore(options, curatedFeedName, id, normalizedVersion: null, return404NotFoundWhenNoResults: false, semVerLevel: semVerLevel); } - private async Task GetCore(ODataQueryOptions options, string curatedFeedName, string id, string version, bool return404NotFoundWhenNoResults) + private async Task GetCore( + ODataQueryOptions options, + string curatedFeedName, + string id, + string normalizedVersion, + bool return404NotFoundWhenNoResults, + string semVerLevel) { var curatedFeed = _entities.CuratedFeeds.FirstOrDefault(cf => cf.Name == curatedFeedName); if (curatedFeed == null) @@ -102,19 +125,27 @@ 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)) + if (!string.IsNullOrEmpty(normalizedVersion)) { - packages = packages.Where(p => p.Version == version); + packages = packages.Where(p => p.NormalizedVersion == normalizedVersion); } + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + // try the search service try { var searchAdaptorResult = await SearchAdaptor.FindByIdAndVersionCore( - _searchService, GetTraditionalHttpContext().Request, packages, id, version, curatedFeed: curatedFeed); + _searchService, + GetTraditionalHttpContext().Request, + packages, + id, + normalizedVersion, + curatedFeed: curatedFeed, + semVerLevel: semVerLevel); // If intercepted, create a paged queryresult if (searchAdaptorResult.ResultsAreProvidedBySearchService) @@ -132,7 +163,7 @@ private async Task GetCore(ODataQueryOptions o var pagedQueryable = packages .Take(options.Top != null ? Math.Min(options.Top.Value, MaxPageSize) : MaxPageSize) - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses, semVerLevelKey); return QueryResult(options, pagedQueryable, MaxPageSize, totalHits, (o, s, resultCount) => SearchAdaptor.GetNextLink(Request.RequestUri, resultCount, new { id }, o, s)); @@ -150,7 +181,11 @@ private async Task GetCore(ODataQueryOptions o return NotFound(); } - var queryable = packages.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + var queryable = packages.ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); + return QueryResult(options, queryable, MaxPageSize); } @@ -167,7 +202,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 +211,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,16 +239,25 @@ 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 var searchAdaptorResult = await SearchAdaptor.SearchCore( - _searchService, GetTraditionalHttpContext().Request, packages, searchTerm, targetFramework, includePrerelease, curatedFeed: curatedFeed); + _searchService, + GetTraditionalHttpContext().Request, + packages, + searchTerm, + targetFramework, + includePrerelease, + curatedFeed: curatedFeed, + semVerLevel: semVerLevel); // Packages provided by search service (even when not hijacked) var query = searchAdaptorResult.Packages; + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + // If intercepted, create a paged queryresult if (searchAdaptorResult.ResultsAreProvidedBySearchService) { @@ -220,7 +265,10 @@ public async Task Search( var totalHits = query.LongCount(); var pagedQueryable = query .Take(options.Top != null ? Math.Min(options.Top.Value, MaxPageSize) : MaxPageSize) - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); return QueryResult(options, pagedQueryable, MaxPageSize, totalHits, (o, s, resultCount) => { @@ -228,14 +276,23 @@ public async Task Search( // Strip it of for backward compatibility. if (o.Top == null || (resultCount.HasValue && o.Top.Value >= resultCount.Value)) { - return SearchAdaptor.GetNextLink(Request.RequestUri, resultCount, new { searchTerm, targetFramework, includePrerelease }, o, s); + return SearchAdaptor.GetNextLink( + Request.RequestUri, + resultCount, + new { searchTerm, targetFramework, includePrerelease }, + o, + s); } return null; }); } // If not, just let OData handle things - var queryable = query.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + var queryable = query.ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); + return QueryResult(options, queryable, MaxPageSize); } @@ -247,9 +304,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..39cdc679cc 100644 --- a/src/NuGetGallery/Controllers/ODataV2FeedController.cs +++ b/src/NuGetGallery/Controllers/ODataV2FeedController.cs @@ -43,18 +43,23 @@ 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()); + + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); // Try the search service try @@ -63,8 +68,13 @@ public async Task Get(ODataQueryOptions option if (_searchService is ExternalSearchService && SearchHijacker.IsHijackable(options, out hijackableQueryParameters)) { var searchAdaptorResult = await SearchAdaptor.FindByIdAndVersionCore( - _searchService, GetTraditionalHttpContext().Request, packages, - hijackableQueryParameters.Id, hijackableQueryParameters.Version, curatedFeed: null); + _searchService, + GetTraditionalHttpContext().Request, + packages, + hijackableQueryParameters.Id, + hijackableQueryParameters.Version, + curatedFeed: null, + semVerLevel: semVerLevel); // If intercepted, create a paged queryresult if (searchAdaptorResult.ResultsAreProvidedBySearchService) @@ -76,7 +86,10 @@ public async Task Get(ODataQueryOptions option var totalHits = packages.LongCount(); var pagedQueryable = packages .Take(options.Top != null ? Math.Min(options.Top.Value, MaxPageSize) : MaxPageSize) - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); return QueryResult(options, pagedQueryable, MaxPageSize, totalHits, (o, s, resultCount) => SearchAdaptor.GetNextLink(Request.RequestUri, resultCount, null, o, s)); @@ -90,67 +103,120 @@ 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))) { return BadRequest(ODataQueryVerifier.GetValidationFailedMessage(options)); } - var queryable = packages.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + var queryable = packages.ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); + 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: SemVerLevelKey.SemVerLevel2, + 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)) { + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + var emptyResult = Enumerable.Empty().AsQueryable() - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); return QueryResult(options, emptyResult, MaxPageSize); } - return await GetCore(options, id, version: null, return404NotFoundWhenNoResults: false); + return await GetCore( + options, + id, + version: null, + semVerLevel: semVerLevel, + return404NotFoundWhenNoResults: false); } - private async Task 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); + } } + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + // try the search service try { var searchAdaptorResult = await SearchAdaptor.FindByIdAndVersionCore( - _searchService, GetTraditionalHttpContext().Request, packages, id, version, curatedFeed: null); + _searchService, + GetTraditionalHttpContext().Request, + packages, + id, + version, + curatedFeed: null, + semVerLevel: semVerLevel); // If intercepted, create a paged queryresult if (searchAdaptorResult.ResultsAreProvidedBySearchService) @@ -168,7 +234,10 @@ private async Task GetCore(ODataQueryOptions o var pagedQueryable = packages .Take(options.Top != null ? Math.Min(options.Top.Value, MaxPageSize) : MaxPageSize) - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); return QueryResult(options, pagedQueryable, MaxPageSize, totalHits, (o, s, resultCount) => SearchAdaptor.GetNextLink(Request.RequestUri, resultCount, new { id }, o, s)); @@ -186,7 +255,11 @@ private async Task GetCore(ODataQueryOptions o return NotFound(); } - var queryable = packages.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + var queryable = packages.ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); + return QueryResult(options, queryable, MaxPageSize); } @@ -211,7 +284,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,17 +308,27 @@ 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(); // todo: search hijack should take options instead of manually parsing query options var searchAdaptorResult = await SearchAdaptor.SearchCore( - _searchService, GetTraditionalHttpContext().Request, packages, searchTerm, targetFramework, includePrerelease, curatedFeed: null); + _searchService, + GetTraditionalHttpContext().Request, + packages, + searchTerm, + targetFramework, + includePrerelease, + curatedFeed: null, + semVerLevel: semVerLevel); // Packages provided by search service (even when not hijacked) var query = searchAdaptorResult.Packages; + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + // If intercepted, create a paged queryresult if (searchAdaptorResult.ResultsAreProvidedBySearchService) { @@ -252,7 +336,10 @@ public async Task Search( var totalHits = query.LongCount(); var pagedQueryable = query .Take(options.Top != null ? Math.Min(options.Top.Value, MaxPageSize) : MaxPageSize) - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); return QueryResult(options, pagedQueryable, MaxPageSize, totalHits, (o, s, resultCount) => { @@ -273,24 +360,34 @@ public async Task Search( } // If not, just let OData handle things - var queryable = query.ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + var queryable = query.ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); + 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 +397,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 +463,18 @@ public IHttpActionResult GetUpdates( idValues.Contains(p.PackageRegistration.Id.ToLower())) .OrderBy(p => p.PackageRegistration.Id); - var queryable = GetUpdates(packages, versionLookup, targetFrameworkValues, includeAllVersions) + var semVerLevelKey = SemVerLevelKey.ForSemVerLevel(semVerLevel); + var queryable = GetUpdates(packages, versionLookup, targetFrameworkValues, includeAllVersions, semVerLevel) .AsQueryable() - .ToV2FeedPackageQuery(GetSiteRoot(), _configurationService.Features.FriendlyLicenses); + .ToV2FeedPackageQuery( + GetSiteRoot(), + _configurationService.Features.FriendlyLicenses, + semVerLevelKey); 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 +484,18 @@ 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 +503,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 +532,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/Controllers/PackagesController.cs b/src/NuGetGallery/Controllers/PackagesController.cs index 9353553c25..c28c0714e2 100644 --- a/src/NuGetGallery/Controllers/PackagesController.cs +++ b/src/NuGetGallery/Controllers/PackagesController.cs @@ -282,13 +282,35 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadF return View(); } - var package = _packageService.FindPackageByIdAndVersion(nuspec.GetId(), nuspec.GetVersion().ToStringSafe()); - if (package != null) + var nuspecVersion = nuspec.GetVersion(); + var existingPackage = _packageService.FindPackageByIdAndVersionStrict(nuspec.GetId(), nuspecVersion.ToStringSafe()); + if (existingPackage != null) { - ModelState.AddModelError( - string.Empty, - string.Format( - CultureInfo.CurrentCulture, Strings.PackageExistsAndCannotBeModified, package.PackageRegistration.Id, package.Version)); + // Determine if the package versions only differ by metadata, + // and provide the most optimal the user-facing error message. + var existingPackageVersion = new NuGetVersion(existingPackage.Version); + if ((existingPackageVersion.HasMetadata || nuspecVersion.HasMetadata) + && !string.Equals(existingPackageVersion.Metadata, nuspecVersion.Metadata)) + { + ModelState.AddModelError( + string.Empty, + string.Format( + CultureInfo.CurrentCulture, + Strings.PackageVersionDiffersOnlyByMetadataAndCannotBeModified, + existingPackage.PackageRegistration.Id, + existingPackage.Version)); + } + else + { + ModelState.AddModelError( + string.Empty, + string.Format( + CultureInfo.CurrentCulture, + Strings.PackageExistsAndCannotBeModified, + existingPackage.PackageRegistration.Id, + existingPackage.Version)); + } + return View(); } @@ -300,7 +322,7 @@ public virtual async Task UploadPackage(HttpPostedFileBase uploadF public virtual async Task DisplayPackage(string id, string version) { - string normalized = NuGetVersionNormalizer.Normalize(version); + string normalized = NuGetVersionFormatter.Normalize(version); if (!string.Equals(version, normalized)) { // Permanent redirect to the normalized one (to avoid multiple URLs for the same content) @@ -310,11 +332,11 @@ public virtual async Task DisplayPackage(string id, string version Package package; if (version != null && version.Equals(Constants.AbsoluteLatestUrlString, StringComparison.InvariantCultureIgnoreCase)) { - package = _packageService.FindAbsoluteLatestPackageById(id); + package = _packageService.FindAbsoluteLatestPackageById(id, SemVerLevelKey.SemVer2); } else { - package = _packageService.FindPackageByIdAndVersion(id, version); + package = _packageService.FindPackageByIdAndVersion(id, version, SemVerLevelKey.SemVer2); } if (package == null) @@ -354,8 +376,11 @@ public virtual async Task DisplayPackage(string id, string version .Normalize(NormalizationForm.FormC); var searchFilter = SearchAdaptor.GetSearchFilter( - "id:\"" + normalizedRegistrationId + "\" AND version:\"" + package.Version + "\"", - 1, null, SearchFilter.ODataSearchContext); + q: "id:\"" + normalizedRegistrationId + "\" AND version:\"" + package.Version + "\"", + page: 1, + sortOrder: null, + context: SearchFilter.ODataSearchContext, + semVerLevel: SemVerLevelKey.SemVerLevel2); searchFilter.IncludePrerelease = true; searchFilter.IncludeAllVersions = true; @@ -415,7 +440,13 @@ public virtual async Task ListPackages(PackageListSearchViewModel var cachedResults = HttpContext.Cache.Get("DefaultSearchResults"); if (cachedResults == null) { - var searchFilter = SearchAdaptor.GetSearchFilter(q, page, null, SearchFilter.UISearchContext); + var searchFilter = SearchAdaptor.GetSearchFilter( + q, + page, + sortOrder: null, + context: SearchFilter.UISearchContext, + semVerLevel: SemVerLevelKey.SemVerLevel2); + results = await _searchService.Search(searchFilter); // note: this is a per instance cache @@ -435,7 +466,13 @@ public virtual async Task ListPackages(PackageListSearchViewModel } else { - var searchFilter = SearchAdaptor.GetSearchFilter(q, page, null, SearchFilter.UISearchContext); + var searchFilter = SearchAdaptor.GetSearchFilter( + q, + page, + sortOrder: null, + context: SearchFilter.UISearchContext, + semVerLevel: SemVerLevelKey.SemVerLevel2); + results = await _searchService.Search(searchFilter); } @@ -472,7 +509,7 @@ public virtual async Task ListPackages(PackageListSearchViewModel [HttpGet] public virtual ActionResult ReportAbuse(string id, string version) { - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { @@ -521,7 +558,7 @@ public virtual ActionResult ReportMyPackage(string id, string version) { var user = GetCurrentUser(); - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { @@ -566,7 +603,7 @@ public virtual async Task ReportAbuse(string id, string version, R return ReportAbuse(id, version); } - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return HttpNotFound(); @@ -624,7 +661,7 @@ public virtual async Task ReportMyPackage(string id, string versio return ReportMyPackage(id, version); } - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return HttpNotFound(); @@ -818,7 +855,7 @@ public virtual async Task Delete(DeletePackagesRequest deletePacka var split = package.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries); if (split.Length == 2) { - var packageToDelete = _packageService.FindPackageByIdAndVersion(split[0], split[1], allowPrerelease: true); + var packageToDelete = _packageService.FindPackageByIdAndVersionStrict(split[0], split[1]); if (packageToDelete != null) { packagesToDelete.Add(packageToDelete); @@ -998,7 +1035,7 @@ public virtual async Task ConfirmOwner(string id, string username, internal virtual async Task Edit(string id, string version, bool? listed, Func urlFactory) { - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return HttpNotFound(); @@ -1064,7 +1101,8 @@ public virtual async Task VerifyPackage() var model = new VerifyPackageRequest { Id = packageMetadata.Id, - Version = packageMetadata.Version.ToNormalizedStringSafe(), + Version = packageMetadata.Version.ToFullStringSafe(), + OriginalVersion = packageMetadata.Version.OriginalVersion, LicenseUrl = packageMetadata.LicenseUrl.ToEncodedUrlStringOrNull(), Listed = true, Language = packageMetadata.Language, @@ -1123,10 +1161,11 @@ public virtual async Task VerifyPackage(VerifyPackageRequest formD // Rule out problem scenario with multiple tabs - verification request (possibly with edits) was submitted by user // viewing a different package to what was actually most recently uploaded - if (!(String.IsNullOrEmpty(formData.Id) || String.IsNullOrEmpty(formData.Version))) + if (!(String.IsNullOrEmpty(formData.Id) || String.IsNullOrEmpty(formData.OriginalVersion))) { if (!(String.Equals(packageMetadata.Id, formData.Id, StringComparison.OrdinalIgnoreCase) - && String.Equals(packageMetadata.Version.ToNormalizedString(), formData.Version, StringComparison.OrdinalIgnoreCase))) + && String.Equals(packageMetadata.Version.ToFullStringSafe(), formData.Version, StringComparison.OrdinalIgnoreCase) + && String.Equals(packageMetadata.Version.OriginalVersion, formData.OriginalVersion, StringComparison.OrdinalIgnoreCase))) { TempData["Message"] = "Your attempt to verify the package submission failed, because the package file appears to have changed. Please try again."; return new RedirectResult(Url.VerifyPackage()); @@ -1218,8 +1257,8 @@ await _auditingService.SaveAuditRecordAsync( // notify user _messageService.SendPackageAddedNotice(package, - Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), - Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.Version }, protocol: Request.Url.Scheme), + Url.Action("DisplayPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.NormalizedVersion }, protocol: Request.Url.Scheme), + Url.Action("ReportMyPackage", "Packages", routeValues: new { id = package.PackageRegistration.Id, version = package.NormalizedVersion }, protocol: Request.Url.Scheme), Url.Action("Account", "Users", routeValues: null, protocol: Request.Url.Scheme)); } @@ -1231,7 +1270,11 @@ await _auditingService.SaveAuditRecordAsync( TempData["Message"] = String.Format( CultureInfo.CurrentCulture, Strings.SuccessfullyUploadedPackage, package.PackageRegistration.Id, package.Version); - return RedirectToRoute(RouteName.DisplayPackage, new { package.PackageRegistration.Id, package.NormalizedVersion }); + return RedirectToRoute(RouteName.DisplayPackage, new + { + id = package.PackageRegistration.Id, + version = package.NormalizedVersion + }); } private async Task SafeCreatePackage(NuGetGallery.User currentUser, Stream uploadFile) @@ -1295,7 +1338,7 @@ public virtual async Task SetLicenseReportVisibility(string id, st internal virtual async Task SetLicenseReportVisibility(string id, string version, bool visible, Func urlFactory) { - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { return HttpNotFound(); diff --git a/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs b/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs index 0e3038713e..70daf45f7a 100644 --- a/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs +++ b/src/NuGetGallery/Infrastructure/Lucene/ExternalSearchService.cs @@ -128,7 +128,8 @@ private async Task SearchCore(SearchFilter filter, bool raw) countOnly: filter.CountOnly, explain: false, getAllVersions: filter.IncludeAllVersions, - supportedFramework: filter.SupportedFramework); + supportedFramework: filter.SupportedFramework, + semVerLevel: filter.SemVerLevel); sw.Stop(); SearchResults results = null; diff --git a/src/NuGetGallery/Infrastructure/Lucene/LuceneIndexingService.cs b/src/NuGetGallery/Infrastructure/Lucene/LuceneIndexingService.cs index d126816b40..4c2346904c 100644 --- a/src/NuGetGallery/Infrastructure/Lucene/LuceneIndexingService.cs +++ b/src/NuGetGallery/Infrastructure/Lucene/LuceneIndexingService.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Threading.Tasks; using Lucene.Net.Index; -using Lucene.Net.Store; using NuGetGallery.Configuration; using NuGetGallery.Diagnostics; using WebBackgrounder; @@ -142,16 +141,16 @@ private List GetPackages(DateTime? lastIndexTime) if (lastIndexTime.HasValue) { - // Retrieve the Latest and LatestStable version of packages if any package for that registration changed since we last updated the index. + // Retrieve the Latest, LatestStable, LatestSemVer2 and LatestStableSemVer2 version of packages if any package for that registration changed since we last updated the index. // We need to do this because some attributes that we index such as DownloadCount are values in the PackageRegistration table that may // update independent of the package. set = set.Where( - p => (p.IsLatest || p.IsLatestStable) && + p => (p.IsLatest || p.IsLatestStable || p.IsLatestSemVer2 || p.IsLatestStableSemVer2) && p.PackageRegistration.Packages.Any(p2 => p2.LastUpdated > lastIndexTime)); } else { - set = set.Where(p => p.IsLatest || p.IsLatestStable); // which implies that p.IsListed by the way! + set = set.Where(p => p.IsLatest || p.IsLatestStable || p.IsLatestSemVer2 || p.IsLatestStableSemVer2); // which implies that p.IsListed by the way! } var list = set diff --git a/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs b/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs index 622babd348..5cf96200d2 100644 --- a/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs +++ b/src/NuGetGallery/Infrastructure/Lucene/LuceneSearchService.cs @@ -58,7 +58,7 @@ private SearchResults SearchCore(SearchFilter searchFilter) int numRecords = searchFilter.Skip + searchFilter.Take; var searcher = new IndexSearcher(_directory, readOnly: true); - var query = ParseQuery(searchFilter); + var query = ParseQuery(searchFilter.SearchTerm); // IF searching by relevance, boost scores by download count. if (searchFilter.SortOrder == SortOrder.Relevance) @@ -67,7 +67,16 @@ private SearchResults SearchCore(SearchFilter searchFilter) query = new CustomScoreQuery(query, downloadCountBooster); } - var filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; + string filterTerm; + if (SemVerLevelKey.ForSemVerLevel(searchFilter.SemVerLevel) == SemVerLevelKey.SemVer2) + { + filterTerm = searchFilter.IncludePrerelease ? "IsLatestSemVer2" : "IsLatestStableSemVer2"; + } + else + { + filterTerm = searchFilter.IncludePrerelease ? "IsLatest" : "IsLatestStable"; + } + Query filterQuery = new TermQuery(new Term(filterTerm, Boolean.TrueString)); if (searchFilter.CuratedFeed != null) { @@ -105,6 +114,8 @@ private static Package PackageFromDoc(Document doc) int packageSize = Int32.Parse(doc.Get("PackageFileSize"), CultureInfo.InvariantCulture); bool isLatest = Boolean.Parse(doc.Get("IsLatest")); bool isLatestStable = Boolean.Parse(doc.Get("IsLatestStable")); + bool isLatestSemVer2 = Boolean.Parse(doc.Get("IsLatestSemVer2")); + bool isLatestStableSemVer2 = Boolean.Parse(doc.Get("IsLatestStableSemVer2")); bool requiresLicenseAcceptance = Boolean.Parse(doc.Get("RequiresLicenseAcceptance")); DateTime created = DateTime.Parse(doc.Get("Created"), CultureInfo.InvariantCulture); DateTime published = DateTime.Parse(doc.Get("Published"), CultureInfo.InvariantCulture); @@ -150,6 +161,8 @@ private static Package PackageFromDoc(Document doc) IconUrl = doc.Get("IconUrl"), IsLatest = isLatest, IsLatestStable = isLatestStable, + IsLatestSemVer2 = isLatestSemVer2, + IsLatestStableSemVer2 = isLatestStableSemVer2, Key = key, Language = doc.Get("Language"), LastUpdated = lastUpdated, @@ -192,13 +205,13 @@ private static PackageDependency CreateDependency(string s) }; } - private static Query ParseQuery(SearchFilter searchFilter) + private static Query ParseQuery(string searchTerm) { // 1. parse the query into field clauses and general terms // We imagine that mostly, field clauses are meant to 'filter' results found searching for general terms. // The resulting clause collections may be empty. var queryParser = new NuGetQueryParser(); - var clauses = queryParser.Parse(searchFilter.SearchTerm).Select(StandardizeSearchTerms).ToList(); + var clauses = queryParser.Parse(searchTerm).Select(StandardizeSearchTerms).ToList(); var fieldSpecificTerms = clauses.Where(a => a.Field != null); var generalTerms = clauses.Where(a => a.Field == null); @@ -225,7 +238,7 @@ private static Query ParseQuery(SearchFilter searchFilter) // b) Id-targeted search? [id:Foo bar] // c) Other Field-targeted search? [author:Foo bar] bool doExactId = !fieldSpecificQueries.Any(); - Query generalQuery = BuildGeneralQuery(doExactId, searchFilter.SearchTerm, analyzer, generalTerms, generalQueries); + Query generalQuery = BuildGeneralQuery(doExactId, searchTerm, analyzer, generalTerms, generalQueries); // IF field targeting is done, we should basically want to AND their field specific queries with all other query terms if (fieldSpecificQueries.Any()) diff --git a/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs b/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs index 38c54feda7..c3eff0e59f 100644 --- a/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs +++ b/src/NuGetGallery/Infrastructure/PackageIndexEntity.cs @@ -136,7 +136,7 @@ public Document ToDocument() document.Add(new Field("Version", Package.Version.ToStringSafe(), Field.Store.YES, Field.Index.NO)); string normalizedVersion = String.IsNullOrEmpty(Package.NormalizedVersion) ? - NuGetVersionNormalizer.Normalize(Package.Version) : + NuGetVersionFormatter.Normalize(Package.Version) : Package.NormalizedVersion; document.Add(new Field("NormalizedVersion", normalizedVersion.ToStringSafe(), Field.Store.YES, Field.Index.NO)); @@ -161,6 +161,8 @@ public Document ToDocument() // Fields meant for filtering, also storing data to avoid hitting SQL while doing searches document.Add(new Field("IsLatest", Package.IsLatest.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("IsLatestStable", Package.IsLatestStable.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); + document.Add(new Field("IsLatestSemVer2", Package.IsLatestSemVer2.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); + document.Add(new Field("IsLatestStableSemVer2", Package.IsLatestStableSemVer2.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); // Fields meant for filtering, sorting document.Add(new Field("PublishedDate", Package.Published.Ticks.ToString(CultureInfo.InvariantCulture), Field.Store.NO, Field.Index.NOT_ANALYZED)); diff --git a/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.Designer.cs b/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.Designer.cs new file mode 100644 index 0000000000..e2f639afc2 --- /dev/null +++ b/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.1.3-40302")] + public sealed partial class AddSemVer2LatestVersionColumns : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddSemVer2LatestVersionColumns)); + + string IMigrationMetadata.Id + { + get { return "201704242001472_AddSemVer2LatestVersionColumns"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.cs b/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.cs new file mode 100644 index 0000000000..3f64a552f8 --- /dev/null +++ b/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.cs @@ -0,0 +1,20 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddSemVer2LatestVersionColumns : DbMigration + { + public override void Up() + { + AddColumn("dbo.Packages", "IsLatestSemVer2", c => c.Boolean(nullable: false)); + AddColumn("dbo.Packages", "IsLatestStableSemVer2", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("dbo.Packages", "IsLatestStableSemVer2"); + DropColumn("dbo.Packages", "IsLatestSemVer2"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.resx b/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.resx new file mode 100644 index 0000000000..4c00f2aadc --- /dev/null +++ b/src/NuGetGallery/Migrations/201704242001472_AddSemVer2LatestVersionColumns.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 2f56762b04..b2b7fe01f3 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -424,16 +424,19 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\..\packages\NuGet.Common.4.3.0-preview1-2507\lib\net45\NuGet.Common.dll + ..\..\packages\NuGet.Common.4.3.0-preview1-2524\lib\net45\NuGet.Common.dll - ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2507\lib\net45\NuGet.Frameworks.dll + ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2524\lib\net45\NuGet.Frameworks.dll - ..\..\packages\NuGet.Packaging.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.dll + ..\..\packages\NuGet.Packaging.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.dll - ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.Core.dll + ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.Core.dll + + + ..\..\packages\NuGet.Protocol.4.3.0-preview1-2524\lib\net45\NuGet.Protocol.dll ..\..\packages\NuGet.Services.KeyVault.1.0.0.0\lib\net45\NuGet.Services.KeyVault.dll @@ -448,11 +451,8 @@ ..\..\packages\NuGet.Services.Platform.Client.3.0.29-r-master\lib\portable-net45+wp80+win\NuGet.Services.Platform.Client.dll True - - ..\..\packages\NuGet.Protocol.4.3.0-preview1-2507\lib\net45\NuGet.Protocol.dll - - ..\..\packages\NuGet.Versioning.4.3.0-preview1-2507\lib\net45\NuGet.Versioning.dll + ..\..\packages\NuGet.Versioning.4.3.0-preview1-2524\lib\net45\NuGet.Versioning.dll False @@ -762,6 +762,10 @@ + + + + @@ -771,6 +775,10 @@ + + + 201704242001472_AddSemVer2LatestVersionColumns.cs + @@ -1467,6 +1475,7 @@ + @@ -1728,6 +1737,9 @@ 201705041614287_UserSecurityPolicies_SubscriptionColumn.cs + + 201704242001472_AddSemVer2LatestVersionColumns.cs + diff --git a/src/NuGetGallery/OData/PackageExtensions.cs b/src/NuGetGallery/OData/PackageExtensions.cs index 556df7484c..1b4bacfe28 100644 --- a/src/NuGetGallery/OData/PackageExtensions.cs +++ b/src/NuGetGallery/OData/PackageExtensions.cs @@ -32,6 +32,8 @@ public static IQueryable ToV1FeedPackageQuery(this IQueryable ToV1FeedPackageQuery(this IQueryable ToV2FeedPackageQuery(this IQueryable packages, string siteRoot, bool includeLicenseReport) + public static IQueryable ToV2FeedPackageQuery( + this IQueryable packages, + string siteRoot, + bool includeLicenseReport, + int? semVerLevelKey) { return ProjectV2FeedPackage( - packages - .Include(p => p.PackageRegistration), - siteRoot, includeLicenseReport); + packages.Include(p => p.PackageRegistration), + siteRoot, + includeLicenseReport, + semVerLevelKey); } // Does the actual projection of a Package object to a V2FeedPackage. // This is in a separate method for testability - internal static IQueryable ProjectV2FeedPackage(this IQueryable packages, string siteRoot, bool includeLicenseReport) + internal static IQueryable ProjectV2FeedPackage( + this IQueryable packages, + string siteRoot, + bool includeLicenseReport, + int? semVerLevelKey) { siteRoot = EnsureTrailingSlash(siteRoot); return packages.Select(p => new V2FeedPackage @@ -79,9 +90,12 @@ internal static IQueryable ProjectV2FeedPackage(this IQueryable

ProjectV2FeedPackage(this IQueryable

WithoutSortOnColumn(this IQueryable feedQuery, string columnName, bool confirmToIgnoreSort=true) + internal static IQueryable WithoutSortOnColumn( + this IQueryable feedQuery, + string columnName, + bool confirmToIgnoreSort = true) { return confirmToIgnoreSort ? feedQuery.InterceptWith(new ODataRemoveSorter(columnName)) : feedQuery; } diff --git a/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs b/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs index df66a87d9f..10f404ba42 100644 --- a/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs +++ b/src/NuGetGallery/OData/QueryAllowed/ODataQueryFilter.cs @@ -31,6 +31,9 @@ public enum ODataOperators Top = 1 << 8 } + internal const string IsLatestVersion = "IsLatestVersion"; + internal const string IsAbsoluteLatestVersion = "IsAbsoluteLatestVersion"; + private static readonly string ResourcesNamespace = "NuGetGallery.OData.QueryAllowed.Data"; private HashSet _allowedOperatorPatterns = null; diff --git a/src/NuGetGallery/OData/QueryInterceptors/NormalizeVersionInterceptor.cs b/src/NuGetGallery/OData/QueryInterceptors/NormalizeVersionInterceptor.cs index 6ed5c7506b..b74dd4e42d 100644 --- a/src/NuGetGallery/OData/QueryInterceptors/NormalizeVersionInterceptor.cs +++ b/src/NuGetGallery/OData/QueryInterceptors/NormalizeVersionInterceptor.cs @@ -26,7 +26,7 @@ protected override Expression VisitBinary(BinaryExpression node) // We have a "Package.Version == " expression! // Transform the constant version into a normalized version - string newVersion = NuGetVersionNormalizer.Normalize((string)constSide.Value); + string newVersion = NuGetVersionFormatter.Normalize((string)constSide.Value); // Create a new expression that checks the new constant against NormalizedVersion instead return Expression.MakeBinary( diff --git a/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs b/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs index 72a411a367..839479f956 100644 --- a/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs +++ b/src/NuGetGallery/OData/SearchService/SearchAdaptor.cs @@ -12,6 +12,7 @@ using System.Web.Routing; using NuGet.Services.Search.Models; using NuGetGallery.Infrastructure.Lucene; +using NuGetGallery.OData.QueryFilter; using NuGetGallery.OData.QueryInterceptors; using QueryInterceptor; @@ -24,14 +25,15 @@ public static class SearchAdaptor /// internal const int MaxPageSize = 100; - public static SearchFilter GetSearchFilter(string q, int page, string sortOrder, string context) + public static SearchFilter GetSearchFilter(string q, int page, string sortOrder, string context, string semVerLevel) { var searchFilter = new SearchFilter(context) { SearchTerm = q, Skip = (page - 1) * Constants.DefaultPackageListPageSize, // pages are 1-based. Take = Constants.DefaultPackageListPageSize, - IncludePrerelease = true + IncludePrerelease = true, + SemVerLevel = semVerLevel }; switch (sortOrder) @@ -93,7 +95,8 @@ public static async Task FindByIdAndVersionCore( IQueryable packages, string id, string version, - CuratedFeed curatedFeed) + CuratedFeed curatedFeed, + string semVerLevel) { SearchFilter searchFilter; // We can only use Lucene if: @@ -112,6 +115,7 @@ public static async Task FindByIdAndVersionCore( } searchFilter.SearchTerm = searchTerm; + searchFilter.SemVerLevel = semVerLevel; searchFilter.IncludePrerelease = true; searchFilter.CuratedFeed = curatedFeed; searchFilter.SupportedFramework = null; @@ -132,7 +136,8 @@ public static async Task SearchCore( string searchTerm, string targetFramework, bool includePrerelease, - CuratedFeed curatedFeed) + CuratedFeed curatedFeed, + string semVerLevel) { SearchFilter searchFilter; // We can only use Lucene if: @@ -144,6 +149,7 @@ public static async Task SearchCore( searchFilter.IncludePrerelease = includePrerelease; searchFilter.CuratedFeed = curatedFeed; searchFilter.SupportedFramework = targetFramework; + searchFilter.SemVerLevel = semVerLevel; var results = await GetResultsFromSearchService(searchService, searchFilter); @@ -154,6 +160,8 @@ public static async Task SearchCore( { packages = packages.Where(p => !p.IsPrerelease); } + + packages = packages.Where(SemVerLevelKey.IsPackageCompliantWithSemVerLevel(semVerLevel)); return new SearchAdaptorResult(false, packages.Search(searchTerm)); } @@ -202,7 +210,9 @@ private static bool TryReadSearchFilter(bool allVersionsInIndex, string url, boo string filter; if (queryTerms.TryGetValue("$filter", out filter)) { - if (!ignoreLatestVersionFilter && !(filter.Equals("IsLatestVersion", StringComparison.Ordinal) || filter.Equals("IsAbsoluteLatestVersion", StringComparison.Ordinal))) + if (!ignoreLatestVersionFilter + && !(filter.Equals(ODataQueryFilter.IsLatestVersion, StringComparison.Ordinal) + || filter.Equals(ODataQueryFilter.IsAbsoluteLatestVersion, StringComparison.Ordinal))) { searchFilter = null; return false; diff --git a/src/NuGetGallery/OData/SearchService/SearchHijacker.cs b/src/NuGetGallery/OData/SearchService/SearchHijacker.cs index 02e4f3f750..31cc0c846e 100644 --- a/src/NuGetGallery/OData/SearchService/SearchHijacker.cs +++ b/src/NuGetGallery/OData/SearchService/SearchHijacker.cs @@ -216,7 +216,7 @@ private static Tuple ExtractComparison(BinaryExpression binExpr) } else if (memberSide.Member == VersionMember) { - return Tuple.Create(Target.Version, NuGetVersionNormalizer.Normalize((string)constSide.Value)); + return Tuple.Create(Target.Version, NuGetVersionFormatter.Normalize((string)constSide.Value)); } else if (memberSide.Member == IdMember) { diff --git a/src/NuGetGallery/OData/Serializers/FeedPackageAnnotationStrategy.cs b/src/NuGetGallery/OData/Serializers/FeedPackageAnnotationStrategy.cs new file mode 100644 index 0000000000..2290e9ed60 --- /dev/null +++ b/src/NuGetGallery/OData/Serializers/FeedPackageAnnotationStrategy.cs @@ -0,0 +1,57 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Data.OData; +using System; +using System.Net.Http; +using System.Web.Http.Routing; + +namespace NuGetGallery.OData.Serializers +{ + internal abstract class FeedPackageAnnotationStrategy + : IFeedPackageAnnotationStrategy + { + private readonly string _contentType; + + protected FeedPackageAnnotationStrategy(string contentType) + { + if (string.IsNullOrEmpty(contentType)) + { + throw new ArgumentException(nameof(contentType)); + } + + _contentType = contentType; + } + + protected string ContentType => _contentType; + + public bool CanAnnotate(object entityInstance) + { + return entityInstance != null && entityInstance is TFeedPackage; + } + + public abstract void Annotate(HttpRequestMessage request, ODataEntry entry, object entityInstance); + + protected static Uri BuildLinkForStreamProperty(string routePrefix, string id, string version, HttpRequestMessage request) + { + var url = new UrlHelper(request); + var result = url.Route(routePrefix + RouteName.DownloadPackage, new { id, version }); + + var builder = new UriBuilder(request.RequestUri); + builder.Path = version == null ? EnsureTrailingSlash(result) : result; + builder.Query = string.Empty; + + return builder.Uri; + } + + private static string EnsureTrailingSlash(string url) + { + if (url != null && !url.EndsWith("/", StringComparison.OrdinalIgnoreCase)) + { + return url + '/'; + } + + return url; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/Serializers/IFeedPackageAnnotationStrategy.cs b/src/NuGetGallery/OData/Serializers/IFeedPackageAnnotationStrategy.cs new file mode 100644 index 0000000000..44500389be --- /dev/null +++ b/src/NuGetGallery/OData/Serializers/IFeedPackageAnnotationStrategy.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Data.OData; +using System.Net.Http; + +namespace NuGetGallery.OData.Serializers +{ + internal interface IFeedPackageAnnotationStrategy + { + bool CanAnnotate(object entityInstance); + void Annotate(HttpRequestMessage request, ODataEntry entry, object entityInstance); + } +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs b/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs index 7c18883fef..28cdec99e5 100644 --- a/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs +++ b/src/NuGetGallery/OData/Serializers/NuGetEntityTypeSerializer.cs @@ -1,13 +1,10 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; -using System.Net.Http; +using Microsoft.Data.OData; +using System.Collections.Generic; using System.Web.Http.OData; using System.Web.Http.OData.Formatter.Serialization; -using System.Web.Http.Routing; -using Microsoft.Data.OData; -using Microsoft.Data.OData.Atom; namespace NuGetGallery.OData.Serializers { @@ -15,110 +12,37 @@ public class NuGetEntityTypeSerializer : ODataEntityTypeSerializer { private readonly string _contentType; + private readonly IReadOnlyCollection _annotationStrategies; public NuGetEntityTypeSerializer(ODataSerializerProvider serializerProvider) : base(serializerProvider) { _contentType = "application/zip"; + _annotationStrategies = new List + { + new V1FeedPackageAnnotationStrategy(_contentType), + new V2FeedPackageAnnotationStrategy(_contentType) + }; } public override ODataEntry CreateEntry(SelectExpandNode selectExpandNode, EntityInstanceContext entityInstanceContext) { var entry = base.CreateEntry(selectExpandNode, entityInstanceContext); - TryAnnotateV1FeedPackage(entry, entityInstanceContext); - TryAnnotateV2FeedPackage(entry, entityInstanceContext); - - return entry; - } - - private void TryAnnotateV1FeedPackage(ODataEntry entry, EntityInstanceContext entityInstanceContext) - { - var instance = entityInstanceContext.EntityInstance as V1FeedPackage; - if (instance != null) + foreach (var annotationStrategy in _annotationStrategies) { - // Set Atom entry metadata - var atomEntryMetadata = new AtomEntryMetadata(); - atomEntryMetadata.Title = instance.Title; - if (!string.IsNullOrEmpty(instance.Authors)) - { - atomEntryMetadata.Authors = new[] { new AtomPersonMetadata { Name = instance.Authors } }; - } - if (instance.LastUpdated > DateTime.MinValue) + if (annotationStrategy.CanAnnotate(entityInstanceContext.EntityInstance)) { - atomEntryMetadata.Updated = instance.LastUpdated; + annotationStrategy.Annotate(entityInstanceContext.Request, entry, entityInstanceContext.EntityInstance); } - if (!string.IsNullOrEmpty(instance.Summary)) - { - atomEntryMetadata.Summary = instance.Summary; - } - entry.SetAnnotation(atomEntryMetadata); - - // Add package download link - entry.MediaResource = new ODataStreamReferenceValue - { - ContentType = ContentType, - ReadLink = BuildLinkForStreamProperty("v1", instance.Id, instance.Version, entityInstanceContext.Request) - }; - } - } - - private void TryAnnotateV2FeedPackage(ODataEntry entry, EntityInstanceContext entityInstanceContext) - { - var instance = entityInstanceContext.EntityInstance as V2FeedPackage; - if (instance != null) - { - // Set Atom entry metadata - var atomEntryMetadata = new AtomEntryMetadata(); - atomEntryMetadata.Title = instance.Id; - if (!string.IsNullOrEmpty(instance.Authors)) - { - atomEntryMetadata.Authors = new[] { new AtomPersonMetadata { Name = instance.Authors } }; - } - if (instance.LastUpdated > DateTime.MinValue) - { - atomEntryMetadata.Updated = instance.LastUpdated; - } - if (!string.IsNullOrEmpty(instance.Summary)) - { - atomEntryMetadata.Summary = instance.Summary; - } - entry.SetAnnotation(atomEntryMetadata); - - // Add package download link - entry.MediaResource = new ODataStreamReferenceValue - { - ContentType = ContentType, - ReadLink = BuildLinkForStreamProperty("v2", instance.Id, instance.Version, entityInstanceContext.Request) - }; } + + return entry; } public string ContentType { get { return _contentType; } } - - private static Uri BuildLinkForStreamProperty(string routePrefix, string id, string version, HttpRequestMessage request) - { - var url = new UrlHelper(request); - var result = url.Route(routePrefix + RouteName.DownloadPackage, new { id, version }); - - var builder = new UriBuilder(request.RequestUri); - builder.Path = version == null ? EnsureTrailingSlash(result) : result; - builder.Query = string.Empty; - - return builder.Uri; - } - - private static string EnsureTrailingSlash(string url) - { - if (url != null && !url.EndsWith("/", StringComparison.OrdinalIgnoreCase)) - { - return url + '/'; - } - - return url; - } } } \ No newline at end of file diff --git a/src/NuGetGallery/OData/Serializers/V1FeedPackageAnnotationStrategy.cs b/src/NuGetGallery/OData/Serializers/V1FeedPackageAnnotationStrategy.cs new file mode 100644 index 0000000000..7c07cb8562 --- /dev/null +++ b/src/NuGetGallery/OData/Serializers/V1FeedPackageAnnotationStrategy.cs @@ -0,0 +1,56 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Data.OData; +using Microsoft.Data.OData.Atom; +using System; +using System.Net.Http; + +namespace NuGetGallery.OData.Serializers +{ + internal class V1FeedPackageAnnotationStrategy + : FeedPackageAnnotationStrategy + { + public V1FeedPackageAnnotationStrategy(string contentType) + : base(contentType) + { + } + + public override void Annotate(HttpRequestMessage request, ODataEntry entry, object entityInstance) + { + var feedPackage = entityInstance as V1FeedPackage; + if (feedPackage == null) + { + return; + } + + // Set Atom entry metadata + var atomEntryMetadata = new AtomEntryMetadata(); + atomEntryMetadata.Title = feedPackage.Title; + + if (!string.IsNullOrEmpty(feedPackage.Authors)) + { + atomEntryMetadata.Authors = new[] { new AtomPersonMetadata { Name = feedPackage.Authors } }; + } + + if (feedPackage.LastUpdated > DateTime.MinValue) + { + atomEntryMetadata.Updated = feedPackage.LastUpdated; + } + + if (!string.IsNullOrEmpty(feedPackage.Summary)) + { + atomEntryMetadata.Summary = feedPackage.Summary; + } + + entry.SetAnnotation(atomEntryMetadata); + + // Add package download link + entry.MediaResource = new ODataStreamReferenceValue + { + ContentType = ContentType, + ReadLink = BuildLinkForStreamProperty("v1", feedPackage.Id, feedPackage.Version, request) + }; + } + } +} \ No newline at end of file diff --git a/src/NuGetGallery/OData/Serializers/V2FeedPackageAnnotationStrategy.cs b/src/NuGetGallery/OData/Serializers/V2FeedPackageAnnotationStrategy.cs new file mode 100644 index 0000000000..64f7128f47 --- /dev/null +++ b/src/NuGetGallery/OData/Serializers/V2FeedPackageAnnotationStrategy.cs @@ -0,0 +1,107 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Data.OData; +using Microsoft.Data.OData.Atom; +using System; +using System.Net.Http; + +namespace NuGetGallery.OData.Serializers +{ + internal class V2FeedPackageAnnotationStrategy + : FeedPackageAnnotationStrategy + { + public V2FeedPackageAnnotationStrategy(string contentType) + : base(contentType) + { + } + + public override void Annotate(HttpRequestMessage request, ODataEntry entry, object entityInstance) + { + var feedPackage = entityInstance as V2FeedPackage; + if (feedPackage == null) + { + return; + } + + // Patch links to use normalized versions + var normalizedVersion = NuGetVersionFormatter.Normalize(feedPackage.Version); + NormalizeNavigationLinks(entry, request, feedPackage, normalizedVersion); + + // Set Atom entry metadata + var atomEntryMetadata = new AtomEntryMetadata(); + atomEntryMetadata.Title = feedPackage.Id; + + if (!string.IsNullOrEmpty(feedPackage.Authors)) + { + atomEntryMetadata.Authors = new[] { new AtomPersonMetadata { Name = feedPackage.Authors } }; + } + + if (feedPackage.LastUpdated > DateTime.MinValue) + { + atomEntryMetadata.Updated = feedPackage.LastUpdated; + } + + if (!string.IsNullOrEmpty(feedPackage.Summary)) + { + atomEntryMetadata.Summary = feedPackage.Summary; + } + + entry.SetAnnotation(atomEntryMetadata); + + // Add package download link + entry.MediaResource = new ODataStreamReferenceValue + { + ContentType = ContentType, + ReadLink = BuildLinkForStreamProperty("v2", feedPackage.Id, normalizedVersion, request) + }; + } + + private static void NormalizeNavigationLinks(ODataEntry entry, HttpRequestMessage request, V2FeedPackage instance, string normalizedVersion) + { + if (entry.Id == null && entry.ReadLink == null && entry.EditLink == null) + { + return; + } + + var idLink = BuildIdLink(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(); + } + } + + private static Uri BuildIdLink(string id, string version, HttpRequestMessage request) + { + var packageIdentityQuery = $"(Id='{id}',Version='{version}')"; + var localPath = request.RequestUri.LocalPath + // Remove closing brackets from Packages collection + .Replace("/GetUpdates", "/Packages") + .Replace("/FindPackagesById", "/Packages") + .Replace("/Search", "/Packages") + .Replace("/Packages()", "/Packages") + // Remove package identity query + .Replace(packageIdentityQuery, string.Empty); + + // Ensure any OData queries remaining are stripped off + var queryStartIndex = localPath.IndexOf('('); + if (queryStartIndex != -1) + { + localPath = localPath.Substring(0, queryStartIndex); + } + + return new Uri($"{request.RequestUri.Scheme}://{request.RequestUri.Host}{localPath}{packageIdentityQuery}"); + } + } +} \ No newline at end of file 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/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs b/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs index 056c912568..c620937409 100644 --- a/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs +++ b/src/NuGetGallery/RequestModels/VerifyPackageRequest.cs @@ -9,7 +9,17 @@ namespace NuGetGallery public class VerifyPackageRequest { public string Id { get; set; } + + ///

+ /// The normalized, full version string (for display purposes). + /// public string Version { get; set; } + + /// + /// The non-normalized, unmodified, original version as defined in the nuspec. + /// + public string OriginalVersion { get; set; } + public string LicenseUrl { get; set; } public bool Listed { get; set; } public EditPackageVersionRequest Edit { get; set; } diff --git a/src/NuGetGallery/Services/DownloadCountObjectMaterializedInterceptor.cs b/src/NuGetGallery/Services/DownloadCountObjectMaterializedInterceptor.cs index 3a782a5da3..510026aeee 100644 --- a/src/NuGetGallery/Services/DownloadCountObjectMaterializedInterceptor.cs +++ b/src/NuGetGallery/Services/DownloadCountObjectMaterializedInterceptor.cs @@ -29,7 +29,7 @@ protected void InterceptPackageMaterialized(Package package) } var packageNormalizedVersion = String.IsNullOrEmpty(package.NormalizedVersion) - ? NuGetVersionNormalizer.Normalize(package.Version) + ? NuGetVersionFormatter.Normalize(package.Version) : package.NormalizedVersion; int downloadCount; diff --git a/src/NuGetGallery/Services/IPackageService.cs b/src/NuGetGallery/Services/IPackageService.cs index 3706b5a76d..d89af6026f 100644 --- a/src/NuGetGallery/Services/IPackageService.cs +++ b/src/NuGetGallery/Services/IPackageService.cs @@ -11,8 +11,26 @@ namespace NuGetGallery public interface IPackageService { PackageRegistration FindPackageRegistrationById(string id); - Package FindPackageByIdAndVersion(string id, string version, bool allowPrerelease = true); - Package FindAbsoluteLatestPackageById(string id); + + /// + /// Gets the package with the given ID and version when exists; + /// otherwise gets the latest package version for the given package ID matching the provided constraints. + /// + /// The package ID. + /// The package version if known; otherwise null to fallback to retrieve the latest version matching filter criteria. + /// The SemVer-level key that determines the SemVer filter to be applied. + /// True indicating pre-release packages are allowed, otherwise false. + /// + Package FindPackageByIdAndVersion(string id, string version, int? semVerLevelKey = null, bool allowPrerelease = true); + + /// + /// Gets the package with the given ID and version when exists; otherwise null. + /// + /// The package ID. + /// The package version. + Package FindPackageByIdAndVersionStrict(string id, string version); + + Package FindAbsoluteLatestPackageById(string id, int? semVerLevelKey); IEnumerable FindPackagesByOwner(User user, bool includeUnlisted); IEnumerable FindPackageRegistrationsByOwner(User user); IEnumerable FindDependentPackages(Package package); diff --git a/src/NuGetGallery/Services/PackageFileService.cs b/src/NuGetGallery/Services/PackageFileService.cs index afaabb3141..f51a040d9a 100644 --- a/src/NuGetGallery/Services/PackageFileService.cs +++ b/src/NuGetGallery/Services/PackageFileService.cs @@ -131,7 +131,7 @@ private static string BuildFileName(Package package) return BuildFileName( package.PackageRegistration.Id, String.IsNullOrEmpty(package.NormalizedVersion) ? - NuGetVersionNormalizer.Normalize(package.Version) : + NuGetVersionFormatter.Normalize(package.Version) : package.NormalizedVersion); } diff --git a/src/NuGetGallery/Services/PackageService.cs b/src/NuGetGallery/Services/PackageService.cs index be3ade7c85..8537a3abf3 100644 --- a/src/NuGetGallery/Services/PackageService.cs +++ b/src/NuGetGallery/Services/PackageService.cs @@ -133,7 +133,7 @@ public async Task CreatePackageAsync(PackageArchiveReader nugetPackage, // Wrap the exception for consistency of this API. throw new InvalidPackageException(exception.Message, exception); } - + var package = CreatePackageFromNuGetPackage(packageRegistration, nugetPackage, packageMetadata, packageStreamMetadata, user); packageRegistration.Packages.Add(package); await UpdateIsLatestAsync(packageRegistration, false); @@ -159,38 +159,66 @@ public virtual PackageRegistration FindPackageRegistrationById(string id) .SingleOrDefault(pr => pr.Id == id); } - public virtual Package FindPackageByIdAndVersion(string id, string version, bool allowPrerelease = true) + public virtual Package FindPackageByIdAndVersion( + string id, + string version, + int? semVerLevelKey = null, + bool allowPrerelease = true) { - if (String.IsNullOrWhiteSpace(id)) + if (string.IsNullOrWhiteSpace(id)) { throw new ArgumentNullException(nameof(id)); } - // Optimization: Every time we look at a package we almost always want to see - // all the other packages with the same ID via the PackageRegistration property. - // This resulted in a gnarly query. - // Instead, we can always query for all packages with the ID. - IEnumerable packagesQuery = _packageRepository.GetAll() - .Include(p => p.LicenseReports) - .Include(p => p.PackageRegistration) - .Where(p => (p.PackageRegistration.Id == id)); - - if (String.IsNullOrEmpty(version) && !allowPrerelease) + Package package = null; + if (!string.IsNullOrEmpty(version)) { - // If there's a specific version given, don't bother filtering by prerelease. You could be asking for a prerelease package. - packagesQuery = packagesQuery.Where(p => !p.IsPrerelease); + package = FindPackageByIdAndVersionStrict(id, version); } - var packageVersions = packagesQuery.ToList(); - - Package package; - if (String.IsNullOrEmpty(version)) + // Package version not found: fallback to latest version. + if (package == null) { - package = packageVersions.FirstOrDefault(p => p.IsLatestStable); + // Optimization: Every time we look at a package we almost always want to see + // all the other packages with the same ID via the PackageRegistration property. + // This resulted in a gnarly query. + // Instead, we can always query for all packages with the ID. + IEnumerable packagesQuery = GetPackagesByIdQueryable(id); + + if (string.IsNullOrEmpty(version) && !allowPrerelease) + { + // If there's a specific version given, don't bother filtering by prerelease. + // You could be asking for a prerelease package. + packagesQuery = packagesQuery.Where(p => !p.IsPrerelease); + } + + var packageVersions = packagesQuery.ToList(); + + // Fallback behavior: collect the latest version. + // Check SemVer-level and allow-prerelease constraints. + if (semVerLevelKey == SemVerLevelKey.SemVer2) + { + package = packageVersions.FirstOrDefault(p => p.IsLatestStableSemVer2); + + if (package == null && allowPrerelease) + { + package = packageVersions.FirstOrDefault(p => p.IsLatestSemVer2); + } + } - if (package == null && allowPrerelease) + // Fallback behavior: collect the latest version. + // If SemVer-level is not defined, + // or SemVer-level = 2.0.0 and no package was marked as SemVer2-latest, + // then check for packages marked as non-SemVer2 latest. + if (semVerLevelKey == SemVerLevelKey.Unknown + || (semVerLevelKey == SemVerLevelKey.SemVer2 && package == null)) { - package = packageVersions.FirstOrDefault(p => p.IsLatest); + package = packageVersions.FirstOrDefault(p => p.IsLatestStable); + + if (package == null && allowPrerelease) + { + package = packageVersions.FirstOrDefault(p => p.IsLatest); + } } // If we couldn't find a package marked as latest, then @@ -200,26 +228,45 @@ public virtual Package FindPackageByIdAndVersion(string id, string version, bool package = packageVersions.OrderByDescending(p => p.Version).FirstOrDefault(); } } - else + + return package; + } + + public virtual Package FindPackageByIdAndVersionStrict(string id, string version) + { + if (string.IsNullOrWhiteSpace(id)) + { + throw new ArgumentNullException(nameof(id)); + } + + if (string.IsNullOrEmpty(version)) { - package = packageVersions.SingleOrDefault( - p => p.PackageRegistration.Id.Equals(id, StringComparison.OrdinalIgnoreCase) && - ( - String.Equals(p.NormalizedVersion, NuGetVersionNormalizer.Normalize(version), StringComparison.OrdinalIgnoreCase) - )); + throw new ArgumentException(nameof(version)); } + + var normalizedVersion = NuGetVersionFormatter.Normalize(version); + + // These string comparisons are case-(in)sensitive depending on SQLServer collation. + // Case-insensitive collation is recommended, e.g. SQL_Latin1_General_CP1_CI_AS. + var package = GetPackagesByIdQueryable(id) + .SingleOrDefault(p => p.NormalizedVersion == normalizedVersion); + return package; } - public virtual Package FindAbsoluteLatestPackageById(string id) + public virtual Package FindAbsoluteLatestPackageById(string id, int? semVerLevelKey) { - var packageVersions = _packageRepository.GetAll() - .Include(p => p.LicenseReports) - .Include(p => p.PackageRegistration) - .Where(p => p.PackageRegistration.Id == id) - .ToList(); + var packageVersions = GetPackagesByIdQueryable(id); - Package package = packageVersions.FirstOrDefault(p => p.IsLatest); + Package package; + if (semVerLevelKey == SemVerLevelKey.SemVer2) + { + package = packageVersions.FirstOrDefault(p => p.IsLatestSemVer2); + } + else + { + package = packageVersions.FirstOrDefault(p => p.IsLatest); + } // If we couldn't find a package marked as latest, then return the most recent one if (package == null) @@ -235,49 +282,87 @@ public IEnumerable FindPackagesByOwner(User user, bool includeUnlisted) // Like DisplayPackage we should prefer to show you information from the latest stable version, // but show you the latest version (potentially latest UNLISTED version) otherwise. + var mergedResults = new Dictionary(StringComparer.OrdinalIgnoreCase); + + MergeLatestPackagesByOwner(user, includeUnlisted, mergedResults); + MergeLatestStablePackagesByOwner(user, includeUnlisted, mergedResults); + + return mergedResults.Values; + } + + private void MergeLatestStablePackagesByOwner(User user, bool includeUnlisted, Dictionary mergedResults) + { IQueryable latestStablePackageVersions = _packageRepository.GetAll() - .Where(p => - p.PackageRegistration.Owners.Any(owner => owner.Key == user.Key) - && p.IsLatestStable) - .Include(p => p.PackageRegistration) - .Include(p => p.PackageRegistration.Owners); - - var latestPackageVersions = _packageRepository.GetAll() - .Where(p => - p.PackageRegistration.Owners.Any(owner => owner.Key == user.Key) - && p.IsLatest) - .Include(p => p.PackageRegistration) - .Include(p => p.PackageRegistration.Owners); + .Where(p => + p.PackageRegistration.Owners.Any(owner => owner.Key == user.Key) + && (p.IsLatestStable || p.IsLatestStableSemVer2)) + .Include(p => p.PackageRegistration) + .Include(p => p.PackageRegistration.Owners); + + foreach (var latestStablePackagesById in latestStablePackageVersions.ToList().GroupBy(p => p.PackageRegistration.Id)) + { + Package latestStablePackage; + if (includeUnlisted) + { + latestStablePackage = latestStablePackagesById.Single(); + } + else + { + latestStablePackage = + latestStablePackagesById.SingleOrDefault(p => p.IsLatestStableSemVer2) + ?? latestStablePackagesById.SingleOrDefault(p => p.IsLatestStable); + } + + mergedResults[latestStablePackage.PackageRegistration.Id] = latestStablePackage; + } + } + private void MergeLatestPackagesByOwner(User user, bool includeUnlisted, Dictionary mergedResults) + { + IQueryable latestPackageVersions; if (includeUnlisted) { latestPackageVersions = _packageRegistrationRepository.GetAll() - .Where(pr => pr.Owners.Where(owner => owner.Username == user.Username).Any()) - .Select(pr => pr.Packages.OrderByDescending(p => p.Version).FirstOrDefault()) - .Include(p => p.PackageRegistration) - .Include(p => p.PackageRegistration.Owners); + .Where(pr => pr.Owners.Where(owner => owner.Username == user.Username).Any()) + .Select(pr => pr.Packages.OrderByDescending(p => p.Version).FirstOrDefault()) + .Where(p => p != null) + .Include(p => p.PackageRegistration) + .Include(p => p.PackageRegistration.Owners); + } + else + { + latestPackageVersions = _packageRepository.GetAll() + .Where(p => + p.PackageRegistration.Owners.Any(owner => owner.Key == user.Key) + && (p.IsLatest || p.IsLatestSemVer2)) + .Include(p => p.PackageRegistration) + .Include(p => p.PackageRegistration.Owners); } - var mergedResults = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var package in latestPackageVersions.Where(p => p != null)) + foreach (var latestPackagesById in latestPackageVersions.ToList().GroupBy(p => p.PackageRegistration.Id)) { - if (mergedResults.ContainsKey(package.PackageRegistration.Id) - && mergedResults[package.PackageRegistration.Id].Created < package.Created) + Package latestPackage; + if (includeUnlisted) { - mergedResults[package.PackageRegistration.Id] = package; + latestPackage = latestPackagesById.Single(); } else { - mergedResults.Add(package.PackageRegistration.Id, package); + latestPackage = + latestPackagesById.SingleOrDefault(p => p.IsLatestSemVer2) + ?? latestPackagesById.Single(p => p.IsLatest); } - } - foreach (var package in latestStablePackageVersions.Where(p => p != null)) - { - mergedResults[package.PackageRegistration.Id] = package; + if (mergedResults.ContainsKey(latestPackage.PackageRegistration.Id) + && mergedResults[latestPackage.PackageRegistration.Id].Created < latestPackage.Created) + { + mergedResults[latestPackage.PackageRegistration.Id] = latestPackage; + } + else + { + mergedResults.Add(latestPackage.PackageRegistration.Id, latestPackage); + } } - - return mergedResults.Values; } public IEnumerable FindPackageRegistrationsByOwner(User user) @@ -303,7 +388,7 @@ where VersionRange.Parse(d.VersionSpec).Satisfies(packageVersion) public async Task PublishPackageAsync(string id, string version, bool commitChanges = true) { - var package = FindPackageByIdAndVersion(id, version); + var package = FindPackageByIdAndVersionStrict(id, version); if (package == null) { @@ -342,7 +427,7 @@ public async Task AddPackageOwnerAsync(PackageRegistration package, User user) _packageOwnerRequestRepository.DeleteOnCommit(request); await _packageOwnerRequestRepository.CommitChangesAsync(); } - + await _auditingService.SaveAuditRecordAsync( new PackageRegistrationAuditRecord(package, AuditedPackageRegistrationAction.AddOwner, user.Username)); } @@ -396,7 +481,7 @@ public async Task MarkPackageListedAsync(Package package, bool commitChanges = t package.LastEdited = DateTime.UtcNow; await UpdateIsLatestAsync(package.PackageRegistration, false); - + await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(package, AuditedPackageAction.List)); if (commitChanges) @@ -488,6 +573,14 @@ public async Task ConfirmPackageOwnerAsync(PackageRegist return ConfirmOwnershipResult.Failure; } + private IQueryable GetPackagesByIdQueryable(string id) + { + return _packageRepository.GetAll() + .Include(p => p.LicenseReports) + .Include(p => p.PackageRegistration) + .Where(p => p.PackageRegistration.Id == id); + } + private PackageRegistration CreateOrGetPackageRegistration(User currentUser, PackageMetadata packageMetadata) { var packageRegistration = FindPackageRegistrationById(packageMetadata.Id); @@ -536,8 +629,8 @@ private Package CreatePackageFromNuGetPackage(PackageRegistration packageRegistr } public virtual Package EnrichPackageFromNuGetPackage( - Package package, - PackageArchiveReader packageArchive, + Package package, + PackageArchiveReader packageArchive, PackageMetadata packageMetadata, PackageStreamMetadata packageStreamMetadata, User user) @@ -547,9 +640,6 @@ public virtual Package EnrichPackageFromNuGetPackage( package.Version = packageMetadata.Version.OriginalVersion; package.NormalizedVersion = packageMetadata.Version.ToNormalizedString(); - // Identify the SemVerLevelKey using the original package version string and package dependencies - package.SemVerLevelKey = SemVerLevelKey.ForPackage(packageMetadata.Version, package.Dependencies); - package.Description = packageMetadata.Description; package.ReleaseNotes = packageMetadata.ReleaseNotes; package.HashAlgorithm = packageStreamMetadata.HashAlgorithm; @@ -595,7 +685,7 @@ public virtual Package EnrichPackageFromNuGetPackage( package.SupportedFrameworks.Add(new PackageFramework { TargetFramework = supportedFramework }); } } - + package.Dependencies = packageMetadata .GetDependencyGroups() .AsPackageDependencyEnumerable() @@ -610,6 +700,9 @@ public virtual Package EnrichPackageFromNuGetPackage( package.FlattenedPackageTypes = package.PackageTypes.Flatten(); + // Identify the SemVerLevelKey using the original package version string and package dependencies + package.SemVerLevelKey = SemVerLevelKey.ForPackage(packageMetadata.Version, package.Dependencies); + return package; } @@ -626,21 +719,6 @@ private static void ValidateNuGetPackageMetadata(PackageMetadata packageMetadata { throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Id", CoreConstants.MaxPackageIdLength); } - if (packageMetadata.Version.IsPrerelease) - { - var release = packageMetadata.Version.Release; - - if (release.Contains(".")) - { - throw new EntityException(Strings.NuGetPackageReleaseVersionWithDot, "Version"); - } - - long temp; - if (long.TryParse(release, out temp)) - { - throw new EntityException(Strings.NuGetPackageReleaseVersionContainsOnlyNumerics, "Version"); - } - } if (packageMetadata.Authors != null && packageMetadata.Authors.Flatten().Length > 4000) { throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Authors", "4000"); @@ -682,7 +760,7 @@ private static void ValidateNuGetPackageMetadata(PackageMetadata packageMetadata throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Title", "256"); } - if (packageMetadata.Version != null && packageMetadata.Version.ToString().Length > 64) + if (packageMetadata.Version != null && packageMetadata.Version.ToFullString().Length > 64) { throw new EntityException(Strings.NuGetPackagePropertyTooLong, "Version", "64"); } @@ -752,31 +830,42 @@ public async Task UpdateIsLatestAsync(PackageRegistration packageRegistration, b } // TODO: improve setting the latest bit; this is horrible. Trigger maybe? - foreach (var pv in packageRegistration.Packages.Where(p => p.IsLatest || p.IsLatestStable)) + var currentUtcTime = DateTime.UtcNow; + foreach (var pv in packageRegistration.Packages.Where(p => p.IsLatest || p.IsLatestStable || p.IsLatestSemVer2 || p.IsLatestStableSemVer2)) { pv.IsLatest = false; pv.IsLatestStable = false; - pv.LastUpdated = DateTime.UtcNow; + pv.IsLatestSemVer2 = false; + pv.IsLatestStableSemVer2 = false; + pv.LastUpdated = currentUtcTime; } // If the last listed package was just unlisted, then we won't find another one - var latestPackage = FindPackage(packageRegistration.Packages, p => !p.Deleted && p.Listed); + var latestPackage = FindPackage( + packageRegistration.Packages, + p => !p.Deleted && p.Listed && p.SemVerLevelKey == SemVerLevelKey.Unknown); + + var latestSemVer2Package = FindPackage( + packageRegistration.Packages, + p => !p.Deleted && p.Listed && (p.SemVerLevelKey == SemVerLevelKey.SemVer2 || p.SemVerLevelKey == SemVerLevelKey.Unknown)); if (latestPackage != null) { latestPackage.IsLatest = true; - latestPackage.LastUpdated = DateTime.UtcNow; + latestPackage.LastUpdated = currentUtcTime; if (latestPackage.IsPrerelease) { // If the newest uploaded package is a prerelease package, we need to find an older package that is // a release version and set it to IsLatest. - var latestReleasePackage = FindPackage(packageRegistration.Packages.Where(p => !p.IsPrerelease && !p.Deleted && p.Listed)); + var latestReleasePackage = FindPackage( + packageRegistration.Packages.Where(p => !p.IsPrerelease && !p.Deleted && p.Listed && p.SemVerLevelKey == SemVerLevelKey.Unknown)); + if (latestReleasePackage != null) { // We could have no release packages latestReleasePackage.IsLatestStable = true; - latestReleasePackage.LastUpdated = DateTime.UtcNow; + latestReleasePackage.LastUpdated = currentUtcTime; } } else @@ -786,6 +875,32 @@ public async Task UpdateIsLatestAsync(PackageRegistration packageRegistration, b } } + if (latestSemVer2Package != null) + { + latestSemVer2Package.IsLatestSemVer2 = true; + latestSemVer2Package.LastUpdated = currentUtcTime; + + if (latestSemVer2Package.IsPrerelease) + { + // If the newest uploaded package is a prerelease package, we need to find an older package that is + // a release version and set it to IsLatest. + var latestSemVer2ReleasePackage = FindPackage( + packageRegistration.Packages.Where(p => !p.IsPrerelease && !p.Deleted && p.Listed && (p.SemVerLevelKey == SemVerLevelKey.SemVer2 || p.SemVerLevelKey == SemVerLevelKey.Unknown))); + + if (latestSemVer2ReleasePackage != null) + { + // We could have no release packages + latestSemVer2ReleasePackage.IsLatestStableSemVer2 = true; + latestSemVer2ReleasePackage.LastUpdated = currentUtcTime; + } + } + else + { + // Only release versions are marked as IsLatestStable. + latestSemVer2Package.IsLatestStableSemVer2 = true; + } + } + if (commitChanges) { await _packageRepository.CommitChangesAsync(); @@ -798,12 +913,13 @@ private static Package FindPackage(IEnumerable packages, Func new NuGetVersion(p.Version)); + NuGetVersion version = packages.Max(p => new NuGetVersion(p.Version)); if (version == null) { return null; } + return packages.First(pv => pv.Version.Equals(version.OriginalVersion, StringComparison.OrdinalIgnoreCase)); } @@ -834,7 +950,7 @@ public async Task SetLicenseReportVisibilityAsync(Package package, bool visible, public async Task IncrementDownloadCountAsync(string id, string version, bool commitChanges = true) { - var package = FindPackageByIdAndVersion(id, version); + var package = FindPackageByIdAndVersionStrict(id, version); if (package != null) { package.DownloadCount++; diff --git a/src/NuGetGallery/Services/ReflowPackageService.cs b/src/NuGetGallery/Services/ReflowPackageService.cs index 6787353359..24df02b23c 100644 --- a/src/NuGetGallery/Services/ReflowPackageService.cs +++ b/src/NuGetGallery/Services/ReflowPackageService.cs @@ -27,7 +27,7 @@ public ReflowPackageService( public async Task ReflowAsync(string id, string version) { - var package = _packageService.FindPackageByIdAndVersion(id, version); + var package = _packageService.FindPackageByIdAndVersionStrict(id, version); if (package == null) { diff --git a/src/NuGetGallery/Services/SearchFilter.cs b/src/NuGetGallery/Services/SearchFilter.cs index d5af032130..ca7afe573a 100644 --- a/src/NuGetGallery/Services/SearchFilter.cs +++ b/src/NuGetGallery/Services/SearchFilter.cs @@ -22,6 +22,8 @@ public class SearchFilter public bool IncludePrerelease { get; set; } + public string SemVerLevel { get; set; } + public CuratedFeed CuratedFeed { get; set; } public SortOrder SortOrder { get; set; } diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index bdf99aeaf3..d57fc0e036 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -19,7 +19,7 @@ namespace NuGetGallery { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] public class Strings { @@ -599,24 +599,6 @@ public static string NuGetPackagePropertyTooLong { } } - /// - /// Looks up a localized string similar to Package {0} invalid: the release label can not only contain numerics.. - /// - public static string NuGetPackageReleaseVersionContainsOnlyNumerics { - get { - return ResourceManager.GetString("NuGetPackageReleaseVersionContainsOnlyNumerics", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Package {0} invalid: no '.' allowed in the release label.. - /// - public static string NuGetPackageReleaseVersionWithDot { - get { - return ResourceManager.GetString("NuGetPackageReleaseVersionWithDot", resourceCulture); - } - } - /// /// Looks up a localized string similar to Package created from API.. /// @@ -671,6 +653,15 @@ public static string PackageIsMissingRequiredData { } } + /// + /// Looks up a localized string similar to Package versions that differ only by metadata cannot be uploaded. A package with ID '{0}' and version '{1}' already exists and cannot be modified.. + /// + public static string PackageVersionDiffersOnlyByMetadataAndCannotBeModified { + get { + return ResourceManager.GetString("PackageVersionDiffersOnlyByMetadataAndCannotBeModified", resourceCulture); + } + } + /// /// Looks up a localized string similar to A package with ID '{0}' and version '{1}' does not exist.. /// diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index 345ce03c82..cd7886f388 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -314,12 +314,6 @@ The {1} Team The NuGet package contains an invalid .nuspec file. The error encountered was: '{0}'. Correct the error and try again. - - Package {0} invalid: no '.' allowed in the release label. - - - Package {0} invalid: the release label can not only contain numerics. - SKIPPED! Package file blob {0} already exists @@ -449,4 +443,7 @@ The {1} Team Your account requires client version '{0}' or higher to be able to push packages. Please contact support@nuget.org to get more details. + + Package versions that differ only by metadata cannot be uploaded. A package with ID '{0}' and version '{1}' already exists and cannot be modified. + \ No newline at end of file diff --git a/src/NuGetGallery/UrlExtensions.cs b/src/NuGetGallery/UrlExtensions.cs index 9fa89746d2..a6b17dda8a 100644 --- a/src/NuGetGallery/UrlExtensions.cs +++ b/src/NuGetGallery/UrlExtensions.cs @@ -110,7 +110,7 @@ public static string Package(this UrlHelper url, string id, string version, stri public static string Package(this UrlHelper url, Package package) { - return url.Package(package.PackageRegistration.Id, package.Version); + return url.Package(package.PackageRegistration.Id, package.NormalizedVersion); } public static string Package(this UrlHelper url, IPackageVersionModel package) diff --git a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs index db2199b6f4..f00441c8e3 100644 --- a/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs +++ b/src/NuGetGallery/ViewModels/ListPackageItemViewModel.cs @@ -38,7 +38,8 @@ public bool UseVersion get { // only use the version in URLs when necessary. This would happen when the latest version is not the same as the latest stable version. - return !(LatestVersion && LatestStableVersion); + return !(!IsSemVer2 && LatestVersion && LatestStableVersion) + && !(IsSemVer2 && LatestStableVersionSemVer2 && LatestVersionSemVer2); } } diff --git a/src/NuGetGallery/ViewModels/PackageViewModel.cs b/src/NuGetGallery/ViewModels/PackageViewModel.cs index a40ba9f691..1e4f56d664 100644 --- a/src/NuGetGallery/ViewModels/PackageViewModel.cs +++ b/src/NuGetGallery/ViewModels/PackageViewModel.cs @@ -3,20 +3,28 @@ using System; using System.Linq; using System.Collections.Generic; +using NuGet.Versioning; namespace NuGetGallery { public class PackageViewModel : IPackageVersionModel { private readonly Package _package; + private readonly bool _isSemVer2; private string _pendingTitle; + private string _fullVersion; public PackageViewModel(Package package) { _package = package; + + _fullVersion = NuGetVersionFormatter.ToFullStringOrFallback(package.Version, fallback: package.Version); + _isSemVer2 = package.SemVerLevelKey == SemVerLevelKey.SemVer2; + Version = String.IsNullOrEmpty(package.NormalizedVersion) ? - NuGetVersionNormalizer.Normalize(package.Version) : + NuGetVersionFormatter.Normalize(package.Version) : package.NormalizedVersion; + Description = package.Description; ReleaseNotes = package.ReleaseNotes; IconUrl = package.IconUrl; @@ -24,7 +32,9 @@ public PackageViewModel(Package package) LicenseUrl = package.LicenseUrl; HideLicenseReport = package.HideLicenseReport; LatestVersion = package.IsLatest; + LatestVersionSemVer2 = package.IsLatestSemVer2; LatestStableVersion = package.IsLatestStable; + LatestStableVersionSemVer2 = package.IsLatestStableSemVer2; LastUpdated = package.Published; Listed = package.Listed; Deleted = package.Deleted; @@ -38,6 +48,7 @@ public PackageViewModel(Package package) LicenseNames = licenseNames.Split(',').Select(l => l.Trim()); } } + public string Description { get; set; } public string ReleaseNotes { get; set; } public string IconUrl { get; set; } @@ -49,6 +60,8 @@ public PackageViewModel(Package package) public DateTime LastUpdated { get; set; } public bool LatestVersion { get; set; } public bool LatestStableVersion { get; set; } + public bool LatestVersionSemVer2 { get; set; } + public bool LatestStableVersionSemVer2 { get; set; } public bool Prerelease { get; set; } public int DownloadCount { get; set; } public bool Listed { get; set; } @@ -65,6 +78,8 @@ public string Id } public string Version { get; set; } + public string FullVersion => _fullVersion; + public bool IsSemVer2 => _isSemVer2; public string Title { diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index 3e7ca37536..459f71d804 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -217,9 +217,17 @@

} + @if (Model.IsSemVer2) + { +

+ This package is only available for download with Visual Studio 2017 (version 15.3) and above or with NuGet client 4.3.0 and above. + Reach out to support if you have more questions. +

+ } +

@Model.Title.Abbreviate(40)

-

@Model.Version

+

@Model.FullVersion

@foreach (var line in Model.Description.ToStringSafe().Split('\n')) { @@ -256,7 +264,7 @@

Install-Package @Model.Id - @if (!Model.LatestVersion || !Model.Listed) + @if (!Model.LatestVersionSemVer2 || !Model.Listed) { -Version @Model.Version } @@ -346,18 +354,18 @@ @foreach (var packageVersion in Model.PackageVersions) { - var rowClass = packageVersion.LatestStableVersion ? "recommended" : null; - var cellTitle = packageVersion.LatestStableVersion ? "Latest Stable Version" : null; + var rowClass = packageVersion.LatestStableVersionSemVer2 ? "recommended" : null; + var cellTitle = packageVersion.LatestStableVersionSemVer2 ? "Latest Stable Version" : null; if (!packageVersion.Deleted && (packageVersion.Listed || (Model.IsOwner(User) || User.IsAdministrator()))) { @if (!packageVersion.IsCurrent(Model)) { - + @packageVersion.Title.Abbreviate(20) @packageVersion.Version - @if (packageVersion.LatestStableVersion) + @if (packageVersion.LatestStableVersionSemVer2) { @:(latest stable) } @@ -365,7 +373,7 @@ } else { - + @packageVersion.Title.Abbreviate(20) @packageVersion.Version (this version) } diff --git a/src/NuGetGallery/Views/Packages/VerifyPackage.cshtml b/src/NuGetGallery/Views/Packages/VerifyPackage.cshtml index ee001c04f7..cf0ac66384 100644 --- a/src/NuGetGallery/Views/Packages/VerifyPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/VerifyPackage.cshtml @@ -36,6 +36,11 @@ } +@helper HiddenField(string formId, string value) +{ + +} + @helper EditableField(string name, Expression> func, bool link = false, bool pre = false) { var formid = ExpressionHelper.GetExpressionText(func).Replace(".", "_"); @@ -79,7 +84,7 @@ Verify Package Details

  • @ReadOnlyField("Package ID", "Id", Model.Id)
  • -
  • @ReadOnlyField("Version", "Version", Model.Version)
  • +
  • @ReadOnlyField("Version", "Version", Model.Version)@HiddenField("OriginalVersion", Model.OriginalVersion)
  • @ReadOnlyField("Minimum NuGet Client Version", "MinClientVersion", Model.MinClientVersion.ToStringSafe())
  • @ReadOnlyField("License URL", "LicenseUrl", Model.LicenseUrl, link: true)
  • @ReadOnlyField("Language", "Language", Model.Language)
  • diff --git a/src/NuGetGallery/Views/Users/Profiles.cshtml b/src/NuGetGallery/Views/Users/Profiles.cshtml index a5d8c61e21..005eeabfed 100644 --- a/src/NuGetGallery/Views/Users/Profiles.cshtml +++ b/src/NuGetGallery/Views/Users/Profiles.cshtml @@ -56,7 +56,7 @@ else if (Model.ShowAllPackages)
    -

    @package.Title Latest version: @package.Version

    +

    @package.Title Latest version: @package.FullVersion

    @if (String.IsNullOrEmpty(package.Description) || package.Description.Length < 350) diff --git a/src/NuGetGallery/WebApi/QueryResult.cs b/src/NuGetGallery/WebApi/QueryResult.cs index 87b8b04f1c..d03e556edb 100644 --- a/src/NuGetGallery/WebApi/QueryResult.cs +++ b/src/NuGetGallery/WebApi/QueryResult.cs @@ -8,25 +8,17 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using System.Web; using System.Web.Http; using System.Web.Http.OData; using System.Web.Http.OData.Query; using System.Web.Http.Results; using Microsoft.Data.OData; using Microsoft.Data.OData.Query; +using NuGetGallery.OData.QueryFilter; namespace NuGetGallery.WebApi { - public static class QueryResultDefaults - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2211:NonConstantFieldsShouldNotBeVisible")] - public static ODataQuerySettings DefaultQuerySettings = new ODataQuerySettings() - { - HandleNullPropagation = HandleNullPropagationOption.False, - EnsureStableOrdering = true, - EnableConstantParameterization = false - }; - } public class QueryResult : IHttpActionResult @@ -37,6 +29,7 @@ public class QueryResult private readonly long? _totalResults; private readonly Func, ODataQuerySettings, long?, Uri> _generateNextLink; private readonly bool _isPagedResult; + private readonly int? _semVerLevelKey; private readonly ODataValidationSettings _validationSettings; private readonly ODataQuerySettings _querySettings; @@ -59,6 +52,9 @@ public QueryResult( _totalResults = totalResults; _generateNextLink = generateNextLink; + var queryDictionary = HttpUtility.ParseQueryString(queryOptions.Request.RequestUri.Query); + _semVerLevelKey = SemVerLevelKey.ForSemVerLevel(queryDictionary["semVerLevel"]); + if (_totalResults.HasValue && generateNextLink != null) { _isPagedResult = true; @@ -92,7 +88,7 @@ public async Task ExecuteAsync(CancellationToken cancellati return response; } - catch(Exception e) + catch (Exception e) { QuietLog.LogHandledException(e); throw; @@ -121,7 +117,22 @@ public IHttpActionResult GetInnerResult() if (queryOptions.Filter != null) { - queryResults = queryOptions.Filter.ApplyTo(queryResults, _querySettings); + if (_semVerLevelKey != SemVerLevelKey.Unknown + && (string.Equals(queryOptions.Filter.RawValue, ODataQueryFilter.IsLatestVersion, StringComparison.OrdinalIgnoreCase) + || string.Equals(queryOptions.Filter.RawValue, ODataQueryFilter.IsAbsoluteLatestVersion, StringComparison.OrdinalIgnoreCase))) + { + // The client uses IsLatestVersion and IsAbsoluteLatestVersion by default, + // and just appends semVerLevel=2.0.0 to the query string. + // When semVerLevel=2.0.0, we should not restrict the filter to only return IsLatest(Stable)=true, + // but also include IsLatest(Stable)SemVer2=true. These additional properties are not exposed on the OData entities however. + // As the proper filtering already should 've happened earlier in the pipeline (SQL or search service), + // the OData filter is redundant, so all we need to do here is to avoid + // the OData filter to be applied on an already correctly filtered result set. + } + else + { + queryResults = queryOptions.Filter.ApplyTo(queryResults, _querySettings); + } } if (queryOptions.OrderBy != null @@ -302,10 +313,10 @@ private IQueryable ExecuteQuery(IQueryable queryable, ODataQueryOptions< { return projection; } - + return queryResult; } - + private BadRequestErrorMessageResult BadRequest(string message) { return new BadRequestErrorMessageResult(message, _controller); diff --git a/src/NuGetGallery/WebApi/QueryResultDefaults.cs b/src/NuGetGallery/WebApi/QueryResultDefaults.cs new file mode 100644 index 0000000000..47e4edb79c --- /dev/null +++ b/src/NuGetGallery/WebApi/QueryResultDefaults.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Web.Http.OData.Query; + +namespace NuGetGallery.WebApi +{ + public static class QueryResultDefaults + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2211:NonConstantFieldsShouldNotBeVisible")] + public static ODataQuerySettings DefaultQuerySettings = new ODataQuerySettings() + { + HandleNullPropagation = HandleNullPropagationOption.False, + EnsureStableOrdering = true, + EnableConstantParameterization = false + }; + } +} \ No newline at end of file diff --git a/src/NuGetGallery/packages.config b/src/NuGetGallery/packages.config index 2b277f9381..e9bd26cbaf 100644 --- a/src/NuGetGallery/packages.config +++ b/src/NuGetGallery/packages.config @@ -77,15 +77,15 @@ - - - - - + + + + + - + diff --git a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj index 756953fe97..93754bfb29 100644 --- a/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj +++ b/tests/NuGetGallery.Core.Facts/NuGetGallery.Core.Facts.csproj @@ -71,22 +71,22 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\..\packages\NuGet.Common.4.3.0-preview1-2507\lib\net45\NuGet.Common.dll + ..\..\packages\NuGet.Common.4.3.0-preview1-2524\lib\net45\NuGet.Common.dll - ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2507\lib\net45\NuGet.Frameworks.dll + ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2524\lib\net45\NuGet.Frameworks.dll ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll - ..\..\packages\NuGet.Packaging.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.dll + ..\..\packages\NuGet.Packaging.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.dll - ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.Core.dll + ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.Core.dll - ..\..\packages\NuGet.Versioning.4.3.0-preview1-2507\lib\net45\NuGet.Versioning.dll + ..\..\packages\NuGet.Versioning.4.3.0-preview1-2524\lib\net45\NuGet.Versioning.dll diff --git a/tests/NuGetGallery.Core.Facts/Packaging/ManifestValidatorFacts.cs b/tests/NuGetGallery.Core.Facts/Packaging/ManifestValidatorFacts.cs index b14860bcc3..f91753a9c3 100644 --- a/tests/NuGetGallery.Core.Facts/Packaging/ManifestValidatorFacts.cs +++ b/tests/NuGetGallery.Core.Facts/Packaging/ManifestValidatorFacts.cs @@ -424,44 +424,39 @@ public void ReturnsErrorIfVersionInvalid2() } [Fact] - public void ReturnsErrorIfVersionIsSemVer200() + public void ReturnsNoErrorIfVersionIsSemVer200() { var nuspecStream = CreateNuspecStream(NuSpecSemVer200); - Assert.Equal(new[] { String.Format(CoreStrings.Manifest_InvalidVersionSemVer200, "2.0.0+123") }, GetErrors(nuspecStream)); + Assert.Empty(GetErrors(nuspecStream)); } - [Theory] - [InlineData("1.0.0-beta.1")] - [InlineData("3.0.0-beta+12")] - public void ReturnsErrorIfDependencyVersionIsSemVer200(string version) + [Fact] + public void ReturnsNoErrorIfDependencyVersionIsSemVer200WithoutMetadataPart() { + const string version = "1.0.0-beta.1"; var nuspecStream = CreateNuspecStream(string.Format(NuSpecDependencyVersionPlaceholder, version)); - - Assert.Equal(new[] { String.Format(CoreStrings.Manifest_InvalidVersionSemVer200, version) }, GetErrors(nuspecStream)); + + Assert.Empty(GetErrors(nuspecStream)); } [Theory] [InlineData("1.0.0-10")] [InlineData("1.0.0--")] - public void ReturnsErrorIfVersionIsInvalid(string version) + public void ReturnsErrorIfNonSemVer2VersionIsNotCompliantWith2XClients(string version) { - // https://github.com/NuGet/NuGetGallery/issues/3226 - var nuspecStream = CreateNuspecStream(string.Format(NuSpecPlaceholderVersion, version)); - Assert.Equal(new[] { String.Format(CoreStrings.Manifest_InvalidVersion, version) }, GetErrors(nuspecStream)); + Assert.Equal(new[] {String.Format(CoreStrings.Manifest_InvalidVersion, version)}, GetErrors(nuspecStream)); } - - [Theory] - [InlineData("1.0.0-10")] - [InlineData("1.0.0--")] - public void ReturnsErrorIfDependencyVersionIsInvalid(string version) + [Fact] + public void ReturnsErrorIfDependencyVersionIsSemVer200WithMetadataPart() { + const string version = "3.0.0-beta+12"; var nuspecStream = CreateNuspecStream(string.Format(NuSpecDependencyVersionPlaceholder, version)); - - Assert.Equal(new[] { String.Format(CoreStrings.Manifest_InvalidVersion, version) }, GetErrors(nuspecStream)); + + Assert.Equal(new[] { String.Format(CoreStrings.Manifest_InvalidDependencyVersion, version) }, GetErrors(nuspecStream)); } [Fact] diff --git a/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs b/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs index 5c6ed4a39f..769673f81c 100644 --- a/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs +++ b/tests/NuGetGallery.Core.Facts/SemVerLevelKeyFacts.cs @@ -93,6 +93,7 @@ public void ReturnsSemVer2ForSemVer2CompliantDependenciesThatAreNotSemVer1Compli [InlineData("1.0.0-alpha")] [InlineData("[1.0-alpha, 2.0.0)")] [InlineData("[1.0, 2.0.0-alpha)")] + [InlineData(null)] public void ReturnsUnknownForNonSemVer2CompliantDependenciesThatAreNotSemVer1Compliant(string versionSpec) { // Arrange @@ -106,5 +107,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.Core.Facts/packages.config b/tests/NuGetGallery.Core.Facts/packages.config index 0a79845190..0e5b572617 100644 --- a/tests/NuGetGallery.Core.Facts/packages.config +++ b/tests/NuGetGallery.Core.Facts/packages.config @@ -9,12 +9,12 @@ - - + + - - - + + + diff --git a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs index 4bbd58382f..0d51dad36e 100644 --- a/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/ApiControllerFacts.cs @@ -661,7 +661,7 @@ public class TheDeletePackageAction public async Task WillThrowIfAPackageWithTheIdAndNuGetVersionDoesNotExist() { var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns((Package)null); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict("theId", "1.0.42")).Returns((Package)null); controller.SetCurrentUser(new User()); var result = await controller.DeletePackage("theId", "1.0.42"); @@ -684,7 +684,7 @@ public async Task WillNotDeleteThePackageIfApiKeyDoesNotBelongToAnOwner() var controller = new TestableApiController(); controller.SetCurrentUser(notOwner); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns(package); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict("theId", "1.0.42")).Returns(package); var result = await controller.DeletePackage("theId", "1.0.42"); @@ -709,7 +709,7 @@ public async Task WillVerifyApiKeyScopeBeforeDelete(string apiKeyScope, bool isD var controller = new TestableApiController(); controller.SetCurrentUser(owner, apiKeyScope); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)) + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict("theId", "1.0.42")) .Returns(package); var result = await controller.DeletePackage("theId", "1.0.42"); @@ -739,7 +739,7 @@ public async Task WillUnlistThePackageIfApiKeyBelongsToAnOwner() PackageRegistration = new PackageRegistration { Owners = new[] { new User(), owner } } }; var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); controller.SetCurrentUser(owner); ResultAssert.IsEmpty(await controller.DeletePackage("theId", "1.0.42")); @@ -778,7 +778,9 @@ public async Task GetPackageReturns404IfPackageIsNotFound() var actionResult = new RedirectResult("http://foo"); var controller = new TestableApiController(MockBehavior.Strict); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(packageId, packageVersion, false)).Returns((Package)null).Verifiable(); + controller.MockPackageService + .Setup(x => x.FindPackageByIdAndVersion(packageId, packageVersion, SemVerLevelKey.SemVer2, false)) + .Returns((Package)null).Verifiable(); controller.MockPackageFileService.Setup(s => s.CreateDownloadPackageActionResultAsync(It.IsAny(), packageId, packageVersion)) .Returns(Task.FromResult(actionResult)) .Verifiable(); @@ -874,7 +876,9 @@ public async Task GetPackageReturnsLatestPackageIfNoVersionIsProvided() var package = new Package() { Version = "1.2.0408", NormalizedVersion = "1.2.408" }; var actionResult = new EmptyResult(); var controller = new TestableApiController(MockBehavior.Strict); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(packageId, "", false)).Returns(package); + controller.MockPackageService + .Setup(x => x.FindPackageByIdAndVersion(packageId, string.Empty, SemVerLevelKey.SemVer2, false)) + .Returns(package); //controller.MockPackageService.Setup(x => x.AddDownloadStatistics(It.IsAny())).Verifiable(); controller.MockPackageFileService.Setup(s => s.CreateDownloadPackageActionResultAsync(HttpRequestUrl, packageId, package.NormalizedVersion)) @@ -913,7 +917,9 @@ public async Task GetPackageReturns503IfNoVersionIsProvidedAndDatabaseUnavailabl var package = new Package(); var actionResult = new EmptyResult(); var controller = new TestableApiController(MockBehavior.Strict); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("Baz", "", false)).Throws(new DataException("Oh noes, database broken!")); + controller.MockPackageService + .Setup(x => x.FindPackageByIdAndVersion("Baz", string.Empty, SemVerLevelKey.SemVer2, false)) + .Throws(new DataException("Oh noes, database broken!")); controller.MockPackageFileService.Setup(s => s.CreateDownloadPackageActionResultAsync(HttpRequestUrl, packageId, package.NormalizedVersion)) .Returns(Task.FromResult(actionResult)) .Verifiable(); @@ -950,7 +956,7 @@ public async Task WillThrowIfAPackageWithTheIdAndNuGetVersionDoesNotExist() { // Arrange var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns((Package)null); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict("theId", "1.0.42")).Returns((Package)null); controller.SetCurrentUser(new User()); // Act @@ -975,7 +981,7 @@ public async Task WillNotListThePackageIfApiKeyDoesNotBelongToAnOwner() }; var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion("theId", "1.0.42", true)).Returns(package); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict("theId", "1.0.42")).Returns(package); controller.SetCurrentUser(owner); // Act @@ -1001,7 +1007,7 @@ public async Task WillListThePackageIfUserIsAnOwner() }; var controller = new TestableApiController(); - controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); + controller.MockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); controller.SetCurrentUser(owner); // Act @@ -1046,7 +1052,9 @@ internal TestableApiController SetupController(string keyType, string scopes, Pa var id = package?.PackageRegistration?.Id ?? "foo"; var version = package?.Version ?? "1.0.0"; - controller.MockPackageService.Setup(s => s.FindPackageByIdAndVersion(id, version, true)).Returns(package); + controller.MockPackageService + .Setup(s => s.FindPackageByIdAndVersion(id, version, SemVerLevelKey.SemVer2, true)) + .Returns(package); controller.SetCurrentUser(user, scopes); diff --git a/tests/NuGetGallery.Facts/Controllers/ODataFeedControllerFactsBase.cs b/tests/NuGetGallery.Facts/Controllers/ODataFeedControllerFactsBase.cs index 7de8da1388..35ea5f7519 100644 --- a/tests/NuGetGallery.Facts/Controllers/ODataFeedControllerFactsBase.cs +++ b/tests/NuGetGallery.Facts/Controllers/ODataFeedControllerFactsBase.cs @@ -122,14 +122,15 @@ private static IQueryable CreatePackagesQueryable() { PackageRegistration = packageRegistration, Version = "1.0.0.0", - NormalizedVersion = "1.0.0.0", + NormalizedVersion = "1.0.0", SemVerLevelKey = SemVerLevelKey.Unknown }, new Package { PackageRegistration = packageRegistration, Version = "1.0.0.0-alpha", - NormalizedVersion = "1.0.0.0-alpha", + NormalizedVersion = "1.0.0-alpha", + IsPrerelease = true, SemVerLevelKey = SemVerLevelKey.Unknown }, new Package @@ -137,20 +138,24 @@ private static IQueryable CreatePackagesQueryable() PackageRegistration = packageRegistration, Version = "2.0.0", NormalizedVersion = "2.0.0", - SemVerLevelKey = SemVerLevelKey.Unknown + SemVerLevelKey = SemVerLevelKey.Unknown, + IsLatestStable = true }, new Package { PackageRegistration = packageRegistration, Version = "2.0.0-alpha", NormalizedVersion = "2.0.0-alpha", - SemVerLevelKey = SemVerLevelKey.SemVer2 + IsPrerelease = true, + SemVerLevelKey = SemVerLevelKey.Unknown, + IsLatest = true }, new Package { PackageRegistration = packageRegistration, Version = "2.0.0-alpha.1", NormalizedVersion = "2.0.0-alpha.1", + IsPrerelease = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }, new Package @@ -158,7 +163,17 @@ private static IQueryable CreatePackagesQueryable() PackageRegistration = packageRegistration, Version = "2.0.0+metadata", NormalizedVersion = "2.0.0", - SemVerLevelKey = SemVerLevelKey.SemVer2 + SemVerLevelKey = SemVerLevelKey.SemVer2, + IsLatestStableSemVer2 = true + }, + new Package + { + PackageRegistration = packageRegistration, + Version = "2.0.1-alpha.1", + NormalizedVersion = "2.0.1-alpha.1", + IsPrerelease = true, + SemVerLevelKey = SemVerLevelKey.SemVer2, + IsLatestSemVer2 = true } }; diff --git a/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs index 4ee610ef06..040de1e892 100644 --- a/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/ODataV1FeedControllerFacts.cs @@ -22,8 +22,8 @@ public async Task Get_FiltersSemVerV2PackageVersions() "/api/v1/Packages"); // Assert - AssertResultCorrect(resultSet); - Assert.Equal(NonSemVer2Packages.Count, resultSet.Count); + AssertSemVer2PackagesFilteredFromResult(resultSet); + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), resultSet.Count); } [Fact] @@ -35,7 +35,7 @@ public async Task GetCount_FiltersSemVerV2PackageVersions() "/api/v1/Packages/$count"); // Assert - Assert.Equal(NonSemVer2Packages.Count, count); + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), count); } [Fact] @@ -47,8 +47,8 @@ public async Task FindPackagesById_FiltersSemVerV2PackageVersions() $"/api/v1/FindPackagesById?id='{TestPackageId}'"); // Assert - AssertResultCorrect(resultSet); - Assert.Equal(NonSemVer2Packages.Count, resultSet.Count); + AssertSemVer2PackagesFilteredFromResult(resultSet); + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), resultSet.Count); } [Fact] @@ -60,8 +60,8 @@ public async Task Search_FiltersSemVerV2PackageVersions() $"/api/v1/Search?searchTerm='{TestPackageId}'"); // Assert - AssertResultCorrect(resultSet); - Assert.Equal(NonSemVer2Packages.Count, resultSet.Count); + AssertSemVer2PackagesFilteredFromResult(resultSet); + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), resultSet.Count); } [Fact] @@ -73,7 +73,7 @@ public async Task SearchCount_FiltersSemVerV2PackageVersions() $"/api/v1/Search/$count?searchTerm='{TestPackageId}'"); // Assert - Assert.Equal(NonSemVer2Packages.Count, searchCount); + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), searchCount); } protected override ODataV1FeedController CreateController(IEntityRepository packagesRepository, @@ -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..2a7efa2565 100644 --- a/tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/ODataV2CuratedFeedControllerFacts.cs @@ -26,15 +26,32 @@ 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, includePrerelease: true); + Assert.Equal(AllPackages.Count(), resultSet.Count); + } + [Fact] public async Task GetCount_FiltersSemVerV2PackageVersions() { // Act - var count = await GetInt( + var count = await GetInt( (controller, options) => controller.GetCount(options, _curatedFeedName), $"/api/v2/curated-feed/{_curatedFeedName}/Packages/$count"); @@ -42,6 +59,22 @@ public async Task GetCount_FiltersSemVerV2PackageVersions() 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,199 @@ 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, includePrerelease: true); + Assert.Equal(AllPackages.Count(), resultSet.Count); + } + [Fact] - public async Task Search_FiltersSemVerV2PackageVersions() + public async Task Search_FiltersSemVerV2PackageVersions_ExcludePrerelease() { // 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.Where(p => !p.IsPrerelease).Count(), resultSet.Count); + } + + [Fact] + public async Task Search_FiltersSemVerV2PackageVersions_IncludePrerelease() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: true), + $"/api/v2/curated-feed/{_curatedFeedName}/Search?searchTerm='{TestPackageId}'&includePrerelease=true"); + + // Assert + 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_ExcludePrerelease(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, includePrerelease: false); + Assert.Equal(AllPackages.Where(p => !p.IsPrerelease).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_IncludePrerelease(string semVerLevel) + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: true, + semVerLevel: semVerLevel), + $"/api/v2/curated-feed/{_curatedFeedName}/Search?searchTerm='{TestPackageId}'?semVerLevel={semVerLevel}&includePrerelease=true"); + + // Assert + AssertSemVer2PackagesIncludedInResult(resultSet, includePrerelease: true); + Assert.Equal(AllPackages.Count(), resultSet.Count); + } + [Fact] - public async Task SearchCount_FiltersSemVerV2PackageVersions() + public async Task SearchCount_FiltersSemVerV2PackageVersions_IncludePrerelease() { // Act var searchCount = await GetInt( - async (controller, options) => await controller.SearchCount(options, _curatedFeedName, TestPackageId), + async (controller, options) => await controller.SearchCount( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: true), + $"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'&includePrerelease=true"); + + // Assert + Assert.Equal(NonSemVer2Packages.Count, searchCount); + } + + [Fact] + public async Task SearchCount_FiltersSemVerV2PackageVersions_ExcludePrerelease() + { + // Act + var searchCount = await GetInt( + async (controller, options) => await controller.SearchCount( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: false), $"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'"); + // Assert + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), searchCount); + } + + [Fact] + public async Task SearchCount_FiltersSemVerV2PackageVersionsWhenSemVerLevelLowerThan200_IncludePrerelease() + { + // Act + var searchCount = await GetInt( + async (controller, options) => await controller.SearchCount( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: true, + semVerLevel: "1.0.0"), + $"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'&includePrerelease=true&semVerLevel=1.0.0"); + // Assert Assert.Equal(NonSemVer2Packages.Count, searchCount); } + [Fact] + public async Task SearchCount_FiltersSemVerV2PackageVersionsWhenSemVerLevelLowerThan200_ExcludePrerelease() + { + // Act + var searchCount = await GetInt( + async (controller, options) => await controller.SearchCount( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: false, + semVerLevel: "1.0.0"), + $"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'&semVerLevel=1.0.0"); + + // Assert + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).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_IncludePrerelease(string semVerLevel) + { + // Act + var searchCount = await GetInt( + async (controller, options) => await controller.SearchCount( + options, + _curatedFeedName, + searchTerm: TestPackageId, + includePrerelease: true, + semVerLevel: semVerLevel), + $"/api/v2/curated-feed/{_curatedFeedName}/Search/$count?searchTerm='{TestPackageId}'&includePrerelease=true&semVerLevel={semVerLevel}"); + + // Assert + Assert.Equal(AllPackages.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_ExcludePrerelease(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.Where(p => !p.IsPrerelease).Count(), searchCount); + } + protected override ODataV2CuratedFeedController CreateController( IEntityRepository packagesRepository, IGalleryConfigurationService configurationService, @@ -115,7 +312,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 +325,28 @@ private void AssertResultCorrect(IEnumerable resultSet) string.Equals(p.PackageRegistration.Id, feedPackage.Id))); } } + + private void AssertSemVer2PackagesIncludedInResult(IReadOnlyCollection resultSet, bool includePrerelease) + { + foreach (var package in SemVer2Packages.Where(p => p.IsPrerelease == includePrerelease)) + { + // 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 => p.IsPrerelease == includePrerelease)) + { + // 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..892ec5adbc 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,15 +23,32 @@ 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( + var count = await GetInt( (controller, options) => controller.GetCount(options), "/api/v2/Packages/$count"); @@ -39,8 +56,24 @@ public async Task GetCount_FiltersSemVerV2PackageVersions() 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,197 @@ 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_IncludePrerelease() { // Act var resultSet = await GetCollection( - async (controller, options) => await controller.Search(options, TestPackageId), - $"/api/v2/Search?searchTerm='{TestPackageId}'"); + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: true), + $"/api/v2/Search?searchTerm='{TestPackageId}'&includePrerelease=true"); // Assert - AssertResultCorrect(resultSet); + AssertSemVer2PackagesFilteredFromResult(resultSet); Assert.Equal(NonSemVer2Packages.Count, resultSet.Count); } [Fact] - public async Task SearchCount_FiltersSemVerV2PackageVersions() + public async Task Search_FiltersSemVerV2PackageVersionsByDefault_ExcludePrerelease() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: false), + $"/api/v2/Search?searchTerm='{TestPackageId}'&includePrerelease=false"); + + // Assert + AssertSemVer2PackagesFilteredFromResult(resultSet); + Assert.Equal(NonSemVer2Packages.Where(p => !p.IsPrerelease).Count(), resultSet.Count); + } + + [Fact] + public async Task SearchIsAbsoluteLatestVersion_ReturnsLatestSemVer2_WhenSemVerLevel200() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: true, + semVerLevel: SemVerLevelKey.SemVerLevel2), + $"/api/v2/Search?$filter=IsAbsoluteLatestVersion&searchTerm='{TestPackageId}'&includePrerelease=true&semVerLevel=2.0.0"); + + // Assert + Assert.Equal(SemVer2Packages.Single(p => p.IsLatestSemVer2).Version, resultSet.Single().Version); + } + + [Fact] + public async Task SearchIsAbsoluteLatestVersion_ReturnsLatest_WhenHigherPrereleaseAvailableAndIncludePrereleaseTrueAndSemVerLevelUndefined() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: true, + semVerLevel: null), + $"/api/v2/Search?$filter=IsAbsoluteLatestVersion&searchTerm='{TestPackageId}'&includePrerelease=true"); + + // Assert + Assert.Equal(NonSemVer2Packages.Single(p => p.IsLatest).Version, resultSet.Single().Version); + } + + [Fact] + public async Task SearchIsLatestVersion_ReturnsLatestStableSemVer2_WhenSemVerLevel200() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: true, + semVerLevel: SemVerLevelKey.SemVerLevel2), + $"/api/v2/Search?$filter=IsLatestVersion&searchTerm='{TestPackageId}'&includePrerelease=true&semVerLevel=2.0.0"); + + // Assert + Assert.Equal(SemVer2Packages.Single(p => p.IsLatestStableSemVer2).Version, resultSet.Single().Version); + } + + [Fact] + public async Task SearchIsLatestVersion_ReturnsLatestStable_WhenIncludePrereleaseFalseAndSemVerLevelUndefined() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: false, + semVerLevel: null), + $"/api/v2/Search?$filter=IsLatestVersion&searchTerm='{TestPackageId}'"); + + // Assert + Assert.Equal(NonSemVer2Packages.Single(p => p.IsLatestStable).Version, resultSet.Single().Version); + } + + [Fact] + public async Task SearchIsLatestVersion_ReturnsLatestStable_WhenIncludePrereleaseFalseAndSemVerLevel100() + { + // Act + var resultSet = await GetCollection( + async (controller, options) => await controller.Search( + options, + searchTerm: TestPackageId, + includePrerelease: false, + semVerLevel: "1.0.0"), + $"/api/v2/Search?$filter=IsLatestVersion&searchTerm='{TestPackageId}'&semVerLevel=1.0.0"); + + // Assert + Assert.Equal(NonSemVer2Packages.Single(p => p.IsLatestStable).Version, resultSet.Single().Version); + } + + [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, + includePrerelease: true, + semVerLevel: semVerLevel), + $"/api/v2/Search?searchTerm='{TestPackageId}'?includePrerelease=true&semVerLevel={semVerLevel}"); + + // Assert + AssertSemVer2PackagesIncludedInResult(resultSet); + Assert.Equal(AllPackages.Count(), resultSet.Count); + } + + [Fact] + public async Task SearchCount_FiltersSemVerV2PackageVersionsByDefault() { // Act var searchCount = await GetInt( - async (controller, options) => await controller.SearchCount(options, TestPackageId), - $"/api/v2/Search/$count?searchTerm='{TestPackageId}'"); + async (controller, options) => await controller.SearchCount( + options, + searchTerm: TestPackageId, + includePrerelease: true), + $"/api/v2/Search/$count?searchTerm='{TestPackageId}'&includePrerelease=true"); // 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, + includePrerelease: true, + semVerLevel: semVerLevel), + $"/api/v2/Search/$count?searchTerm='{TestPackageId}'&includePrerelease=true&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 +284,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 +346,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 +381,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 +394,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/PackagesControllerFacts.cs b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs index 07783ef0bf..a6ba9574a0 100644 --- a/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs +++ b/tests/NuGetGallery.Facts/Controllers/PackagesControllerFacts.cs @@ -190,7 +190,7 @@ public async Task GivenANonExistantPackageIt404s() var packageService = new Mock(); var controller = CreateController(packageService: packageService); - packageService.Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", true)) + packageService.Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", SemVerLevelKey.SemVer2, true)) .ReturnsNull(); // Act @@ -210,7 +210,7 @@ public async Task GivenAValidPackageThatTheCurrentUserDoesNotOwnItDisplaysCurren packageService: packageService, indexingService: indexingService); controller.SetCurrentUser(TestUtility.FakeUser); - packageService.Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", true)) + packageService.Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", SemVerLevelKey.SemVer2, true)) .Returns(new Package() { PackageRegistration = new PackageRegistration() @@ -268,7 +268,7 @@ public async Task GivenAValidPackageThatTheCurrentUserOwnsItDisablesResponseCach }; packageService - .Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", true)) + .Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", SemVerLevelKey.SemVer2, true)) .Returns(package); // Act @@ -308,7 +308,7 @@ public async Task GivenAValidPackageThatTheCurrentUserOwnsWithNoEditsItDisplaysC }; packageService - .Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", true)) + .Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", SemVerLevelKey.SemVer2, true)) .Returns(package); editPackageService .Setup(e => e.GetPendingMetadata(package)) @@ -355,7 +355,7 @@ public async Task GivenAValidPackageThatTheCurrentUserOwnsWithEditsItDisplaysEdi }; packageService - .Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", true)) + .Setup(p => p.FindPackageByIdAndVersion("Foo", "1.1.1", SemVerLevelKey.SemVer2, true)) .Returns(package); editPackageService .Setup(e => e.GetPendingMetadata(package)) @@ -387,7 +387,7 @@ public async Task GivenAnAbsoluteLatestVersionItQueriesTheCorrectVersion() controller.SetCurrentUser(TestUtility.FakeUser); packageService - .Setup(p => p.FindAbsoluteLatestPackageById("Foo")) + .Setup(p => p.FindAbsoluteLatestPackageById("Foo", SemVerLevelKey.SemVer2)) .Returns(new Package() { PackageRegistration = new PackageRegistration() @@ -425,7 +425,7 @@ public async Task GivenAValidPackageWithNoVersionThatTheCurrentUserDoesNotOwnItD packageService: packageService, indexingService: indexingService); controller.SetCurrentUser(TestUtility.FakeUser); - packageService.Setup(p => p.FindPackageByIdAndVersion("Foo", null, true)) + packageService.Setup(p => p.FindPackageByIdAndVersion("Foo", null, SemVerLevelKey.SemVer2, true)) .Returns(new Package() { PackageRegistration = new PackageRegistration() @@ -620,7 +620,7 @@ public async Task UpdatesUnlistedIfSelected() .Throws(new Exception("Shouldn't be called")); packageService.Setup(svc => svc.MarkPackageUnlistedAsync(It.IsAny(), It.IsAny())) .Returns(Task.FromResult(0)).Verifiable(); - packageService.Setup(svc => svc.FindPackageByIdAndVersion("Foo", "1.0", true)) + packageService.Setup(svc => svc.FindPackageByIdAndVersionStrict("Foo", "1.0")) .Returns(package).Verifiable(); var indexingService = new Mock(); @@ -656,7 +656,7 @@ public async Task UpdatesUnlistedIfNotSelected() .Returns(Task.FromResult(0)).Verifiable(); packageService.Setup(svc => svc.MarkPackageUnlistedAsync(It.IsAny(), It.IsAny())) .Throws(new Exception("Shouldn't be called")); - packageService.Setup(svc => svc.FindPackageByIdAndVersion("Foo", "1.0", true)) + packageService.Setup(svc => svc.FindPackageByIdAndVersionStrict("Foo", "1.0")) .Returns(package).Verifiable(); var indexingService = new Mock(); @@ -708,7 +708,7 @@ public async Task SendsMessageToGalleryOwnerWithEmailOnlyWhenUnauthenticated() Version = "2.0.1" }; var packageService = new Mock(); - packageService.Setup(p => p.FindPackageByIdAndVersion("mordor", "2.0.1", true)).Returns(package); + packageService.Setup(p => p.FindPackageByIdAndVersionStrict("mordor", "2.0.1")).Returns(package); var httpContext = new Mock(); var controller = CreateController( packageService: packageService, @@ -749,7 +749,7 @@ public async Task SendsMessageToGalleryOwnerWithUserInfoWhenAuthenticated() Version = "2.0.1" }; var packageService = new Mock(); - packageService.Setup(p => p.FindPackageByIdAndVersion("mordor", It.IsAny(), true)).Returns(package); + packageService.Setup(p => p.FindPackageByIdAndVersionStrict("mordor", It.IsAny())).Returns(package); var httpContext = new Mock(); var controller = CreateController( packageService: packageService, @@ -785,7 +785,7 @@ public void FormRedirectsPackageOwnerToReportMyPackage() Version = "2.0.1" }; var packageService = new Mock(); - packageService.Setup(p => p.FindPackageByIdAndVersion("Mordor", It.IsAny(), true)).Returns(package); + packageService.Setup(p => p.FindPackageByIdAndVersionStrict("Mordor", It.IsAny())).Returns(package); var httpContext = new Mock(); var controller = CreateController( packageService: packageService, @@ -810,7 +810,7 @@ public async Task HtmlEncodesMessageContent() Version = "2.0.1" }; var packageService = new Mock(); - packageService.Setup(p => p.FindPackageByIdAndVersion("mordor", "2.0.1", true)).Returns(package); + packageService.Setup(p => p.FindPackageByIdAndVersionStrict("mordor", "2.0.1")).Returns(package); var httpContext = new Mock(); httpContext.Setup(h => h.Request.IsAuthenticated).Returns(false); var controller = CreateController( @@ -851,7 +851,7 @@ public void FormRedirectsNonOwnersToReportAbuse() }; var user = new User { EmailAddress = "frodo@hobbiton.example.com", Username = "Frodo", Key = 2 }; var packageService = new Mock(); - packageService.Setup(p => p.FindPackageByIdAndVersion("Mordor", It.IsAny(), true)).Returns(package); + packageService.Setup(p => p.FindPackageByIdAndVersionStrict("Mordor", It.IsAny())).Returns(package); var httpContext = new Mock(); var controller = CreateController( packageService: packageService, @@ -874,7 +874,7 @@ public async Task HtmlEncodesMessageContent() Version = "2.0.1" }; var packageService = new Mock(); - packageService.Setup(p => p.FindPackageByIdAndVersion("mordor", "2.0.1", true)).Returns(package); + packageService.Setup(p => p.FindPackageByIdAndVersionStrict("mordor", "2.0.1")).Returns(package); ReportPackageRequest reportRequest = null; var messageService = new Mock(); @@ -1093,8 +1093,8 @@ public async Task WillShowTheViewWithErrorsWhenThePackageAlreadyExists() var fakeFileStream = TestPackage.CreateTestPackageStream("theId", "1.0.0"); fakeUploadedFile.Setup(x => x.InputStream).Returns(fakeFileStream); var fakePackageService = new Mock(); - fakePackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), It.IsAny())).Returns( - new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "theVersion" }); + fakePackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns( + new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "1.0.0" }); var controller = CreateController( packageService: fakePackageService); controller.SetCurrentUser(TestUtility.FakeUser); @@ -1104,7 +1104,30 @@ public async Task WillShowTheViewWithErrorsWhenThePackageAlreadyExists() Assert.NotNull(result); Assert.False(controller.ModelState.IsValid); Assert.Equal( - String.Format(Strings.PackageExistsAndCannotBeModified, "theId", "theVersion"), + String.Format(Strings.PackageExistsAndCannotBeModified, "theId", "1.0.0"), + controller.ModelState[String.Empty].Errors[0].ErrorMessage); + } + + [Fact] + public async Task WillShowTheViewWithErrorsWhenThePackageAlreadyExistsAndOnlyDiffersByMetadata() + { + var fakeUploadedFile = new Mock(); + fakeUploadedFile.Setup(x => x.FileName).Returns("theFile.nupkg"); + var fakeFileStream = TestPackage.CreateTestPackageStream("theId", "1.0.0+metadata2"); + fakeUploadedFile.Setup(x => x.InputStream).Returns(fakeFileStream); + var fakePackageService = new Mock(); + fakePackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns( + new Package { PackageRegistration = new PackageRegistration { Id = "theId" }, Version = "1.0.0+metadata" }); + var controller = CreateController( + packageService: fakePackageService); + controller.SetCurrentUser(TestUtility.FakeUser); + + var result = await controller.UploadPackage(fakeUploadedFile.Object) as ViewResult; + + Assert.NotNull(result); + Assert.False(controller.ModelState.IsValid); + Assert.Equal( + String.Format(Strings.PackageVersionDiffersOnlyByMetadataAndCannotBeModified, "theId", "1.0.0+metadata"), controller.ModelState[String.Empty].Errors[0].ErrorMessage); } @@ -2015,7 +2038,7 @@ public async Task IndexingAndPackageServicesAreUpdated() .Throws(new Exception("Shouldn't be called")); packageService.Setup(svc => svc.SetLicenseReportVisibilityAsync(It.IsAny(), It.Is(t => t == false), It.IsAny())) .Returns(Task.CompletedTask).Verifiable(); - packageService.Setup(svc => svc.FindPackageByIdAndVersion("Foo", "1.0", true)) + packageService.Setup(svc => svc.FindPackageByIdAndVersionStrict("Foo", "1.0")) .Returns(package).Verifiable(); var httpContext = new Mock(); diff --git a/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs b/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs index 8db43c6de0..48dae28d49 100644 --- a/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Infrastructure/LuceneSearchServiceFacts.cs @@ -14,8 +14,10 @@ namespace NuGetGallery.Infrastructure public class LuceneSearchServiceFacts { // This works because we index the description - [Fact] - public void IndexAndSearchAPackageByDescription() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void IndexAndSearchAPackageByDescription(string semVerLevel) { var packages = new List { @@ -31,7 +33,9 @@ public void IndexAndSearchAPackageByDescription() Description = "Package #1 is an awesome package", Listed = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, IsLatest = true, + IsLatestSemVer2 = true, IsPrerelease = true, DownloadCount = 100, FlattenedAuthors = "", @@ -42,15 +46,17 @@ public void IndexAndSearchAPackageByDescription() } }; - var results = IndexAndSearch(packages, "awesome"); + var results = IndexAndSearch(packages, "awesome", semVerLevel); Assert.Single(results); Assert.Equal(3, results[0].Key); Assert.Equal(1, results[0].PackageRegistrationKey); } - [Fact] - public void ResultsIncludeVersionAndNormalizedVersion() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void ResultsIncludeVersionAndNormalizedVersion(string semVerLevel) { var packages = new List { @@ -67,8 +73,10 @@ public void ResultsIncludeVersionAndNormalizedVersion() Title = "Package #1 4.2.0", Description = "Package #1 is an awesome package", Listed = true, - IsLatestStable = true, IsLatest = true, + IsLatestSemVer2 = true, + IsLatestStable = true, + IsLatestStableSemVer2 = true, IsPrerelease = true, DownloadCount = 100, FlattenedAuthors = "", @@ -79,15 +87,17 @@ public void ResultsIncludeVersionAndNormalizedVersion() } }; - var results = IndexAndSearch(packages, "awesome"); + var results = IndexAndSearch(packages, "awesome", semVerLevel); Assert.Single(results); Assert.Equal("01.02.03", results[0].Version); Assert.Equal("1.2.3", results[0].NormalizedVersion); } - [Fact] - public void ResultsIncludeVersionAndNormalizedVersionEvenIfNormalizedVersionColumnNull() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void ResultsIncludeVersionAndNormalizedVersionEvenIfNormalizedVersionColumnNull(string semVerLevel) { var packages = new List { @@ -103,8 +113,10 @@ public void ResultsIncludeVersionAndNormalizedVersionEvenIfNormalizedVersionColu Title = "Package #1 4.2.0", Description = "Package #1 is an awesome package", Listed = true, - IsLatestStable = true, IsLatest = true, + IsLatestSemVer2 = true, + IsLatestStable = true, + IsLatestStableSemVer2 = true, IsPrerelease = true, DownloadCount = 100, FlattenedAuthors = "", @@ -115,7 +127,7 @@ public void ResultsIncludeVersionAndNormalizedVersionEvenIfNormalizedVersionColu } }; - var results = IndexAndSearch(packages, "awesome"); + var results = IndexAndSearch(packages, "awesome", semVerLevel); Assert.Single(results); Assert.Equal("01.02.03", results[0].Version); @@ -123,8 +135,10 @@ public void ResultsIncludeVersionAndNormalizedVersionEvenIfNormalizedVersionColu } // This works because we do some wildcard magic in our searches. - [Fact] - public void IndexAndSearchDavid123For12() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void IndexAndSearchDavid123For12(string semVerLevel) { var packages = new List { @@ -141,21 +155,25 @@ public void IndexAndSearchDavid123For12() Description = "Description", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "DavidX", Title = "DavidTest123", Version = "1.1", } }; - var results = IndexAndSearch(packages, "12"); + var results = IndexAndSearch(packages, "12", semVerLevel); Assert.Single(results); Assert.Equal("DavidTest123", results[0].Title); } - [Fact] - public void IndexAndSearchWithWordStemming() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void IndexAndSearchWithWordStemming(string semVerLevel) { var packages = new List { @@ -179,14 +197,16 @@ public void IndexAndSearchWithWordStemming() } }; - var results = IndexAndSearch(packages, "compressed"); + var results = IndexAndSearch(packages, "compressed", semVerLevel); Assert.Empty(results); // currently stemming is not working //Assert.Equal("SuperzipLib", results[0].Title); } - [Fact] - public void SearchUsingCombinedIdAndGeneralTerms() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void SearchUsingCombinedIdAndGeneralTerms(string semVerLevel) { var packages = new List { @@ -203,7 +223,9 @@ public void SearchUsingCombinedIdAndGeneralTerms() Description = "Yeah", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Eric I", Title = "Red Death", Version = "1.1.2", @@ -221,21 +243,25 @@ public void SearchUsingCombinedIdAndGeneralTerms() Description = "Library for compressing your filez", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Eric II", Title = "Red Herring", Version = "1.1.2", }, }; - var results = IndexAndSearch(packages, "Id:Red Death"); + var results = IndexAndSearch(packages, "Id:Red Death", semVerLevel); Assert.Equal(1, results.Count); Assert.Equal("Red Death", results[0].Title); } - [Fact] - public void SearchUsingExactPackageId() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void SearchUsingExactPackageId(string semVerLevel) { var packages = new List { @@ -253,7 +279,9 @@ public void SearchUsingExactPackageId() DownloadCount = 3, Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "M S C", Tags = "NuGetTag", Title = "NuGet.Core", @@ -274,7 +302,9 @@ public void SearchUsingExactPackageId() DownloadCount = 3, Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Laugh", Title = "SomeotherNuGet.Core.SimilarlyNamedPackage", Version = "1.5.20902.9026", @@ -282,7 +312,7 @@ public void SearchUsingExactPackageId() }; // simple query - var results = IndexAndSearch(packages, "NuGet.Core"); + var results = IndexAndSearch(packages, "NuGet.Core", semVerLevel); Assert.Equal(2, results.Count); Assert.Equal("NuGet.Core", results[0].Title); Assert.Equal(144, results[0].Key); @@ -326,7 +356,9 @@ public void SearchForNuGetCoreWithExactField(string field, string term) Description = "NuGet.Core is the core framework assembly for NuGet that the rest of NuGet builds upon.", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Alpha Beta Gamma", Title = "NuGet.Core", Version = "1.5.20902.9026", @@ -355,14 +387,21 @@ public void SearchForNuGetCoreWithExactField(string field, string term) }; // query targeted specifically against id field should work equally well - var results = IndexAndSearch(packages, field + ":" + term); + var results = IndexAndSearch(packages, field + ":" + term, semVerLevel: null); + Assert.NotEmpty(results); + Assert.Equal("NuGet.Core", results[0].Title); + Assert.Equal("NuGet.Core", results[0].PackageRegistration.Id); + + results = IndexAndSearch(packages, field + ":" + term, semVerLevel: "2.0.0"); Assert.NotEmpty(results); Assert.Equal("NuGet.Core", results[0].Title); Assert.Equal("NuGet.Core", results[0].PackageRegistration.Id); } - [Fact] - public void SearchForJQueryUICombinedWithPartialId() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void SearchForJQueryUICombinedWithPartialId(string semVerLevel) { var packages = new List { @@ -379,20 +418,24 @@ public void SearchForJQueryUICombinedWithPartialId() Description = "jQuery UI is etc etc and many more important things", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Alpha Beta Gamma", Title = "JQuery UI (Combined Blobbary)", Tags = "web javascript", }, }; - var results = IndexAndSearch(packages, "id:JQuery.ui"); + var results = IndexAndSearch(packages, "id:JQuery.ui", semVerLevel); Assert.NotEmpty(results); Assert.Equal("JQuery.UI.Combined", results[0].PackageRegistration.Id); } - [Fact] - public void SearchForDegenerateSingleQuoteQuery() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void SearchForDegenerateSingleQuoteQuery(string semVerLevel) { var packages = new List { @@ -409,20 +452,24 @@ public void SearchForDegenerateSingleQuoteQuery() Description = "jQuery UI is etc etc and many more important things", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Alpha Beta Gamma", Title = "JQuery UI (Combined Blobbary)", Tags = "web javascript", }, }; - var results = IndexAndSearch(packages, "\""); + var results = IndexAndSearch(packages, "\"", semVerLevel); Assert.NotEmpty(results); Assert.Equal("JQuery.UI.Combined", results[0].PackageRegistration.Id); } - [Fact] - public void SearchUsesPackageRegistrationDownloadCountsToPrioritize() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void SearchUsesPackageRegistrationDownloadCountsToPrioritize(string semVerLevel) { var packages = new List { @@ -439,8 +486,10 @@ public void SearchUsesPackageRegistrationDownloadCountsToPrioritize() Description = "FooQuery is overall much less popular than JQuery UI", DownloadCount = 5, Listed = true, - IsLatest = true, - IsLatestStable = true, + IsLatest = true, + IsLatestSemVer2 = true, + IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Alpha Beta Gamma", Title = "FooQuery", Tags = "web javascript", @@ -458,22 +507,26 @@ public void SearchUsesPackageRegistrationDownloadCountsToPrioritize() DownloadCount = 3, Description = "jQuery UI has only a few downloads of its latest and greatest version, but many total downloads", Listed = true, - IsLatest = true, - IsLatestStable = true, + IsLatest = true, + IsLatestSemVer2 = true, + IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Alpha Beta Gamma", Title = "JQuery UI (Combined Blobbary)", Tags = "web javascript", }, }; - var results = IndexAndSearch(packages, ""); + var results = IndexAndSearch(packages, string.Empty, semVerLevel); Assert.NotEmpty(results); Assert.Equal("JQuery.UI.Combined", results[0].PackageRegistration.Id); Assert.Equal("FooQuery", results[1].PackageRegistration.Id); } - [Fact] - public void IndexAndSearchRetrievesCanDriveV2Feed() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void IndexAndSearchRetrievesCanDriveV2Feed(string semVerLevel) { Package p = new Package { @@ -495,7 +548,9 @@ public void IndexAndSearchRetrievesCanDriveV2Feed() // This is a test hash Hash = "Ii4+Gr44RAClAno38k5MYAkcBE6yn2LE2xO+/ViKco45+hoxtwKAytmPWEMCJWhH8FyitjebvS5Fsf+ixI5xIg==", IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, IsPrerelease = false, Language = "en", LastUpdated = DateTime.UtcNow, @@ -522,8 +577,11 @@ public void IndexAndSearchRetrievesCanDriveV2Feed() }; var packages = new[] { p }; - var results = IndexAndSearch(packages, ""); - var r = results.AsQueryable().ToV2FeedPackageQuery("http://www.nuget.org/", true).First(); + var results = IndexAndSearch(packages, string.Empty, semVerLevel); + var r = results.AsQueryable().ToV2FeedPackageQuery( + "http://www.nuget.org/", + includeLicenseReport: true, + semVerLevelKey: SemVerLevelKey.Unknown).First(); Assert.Equal("Pride", r.Id); Assert.Equal("3.4 RC", r.Version); @@ -556,8 +614,10 @@ public void IndexAndSearchRetrievesCanDriveV2Feed() } // See issue https://github.com/NuGet/NuGetGallery/issues/406 - [Fact] - public void SearchWorksAroundLuceneQuerySyntaxExceptions() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void SearchWorksAroundLuceneQuerySyntaxExceptions(string semVerLevel) { var packages = new List { @@ -574,7 +634,9 @@ public void SearchWorksAroundLuceneQuerySyntaxExceptions() Description = "NuGet.Core is the core framework assembly for NuGet that the rest of NuGet builds upon.", Listed = true, IsLatest = true, + IsLatestSemVer2 = true, IsLatestStable = true, + IsLatestStableSemVer2 = true, FlattenedAuthors = "Alpha Beta Gamma", LicenseUrl = "http://nuget.codeplex.com/license", Title = "NuGet.Core", @@ -582,11 +644,11 @@ public void SearchWorksAroundLuceneQuerySyntaxExceptions() }, }; - var results = IndexAndSearch(packages, "*Core"); // Lucene parser throws for leading asterisk in searches + var results = IndexAndSearch(packages, "*Core", semVerLevel); // Lucene parser throws for leading asterisk in searches Assert.NotEmpty(results); } - private IList IndexAndSearch(IEnumerable packages, string searchTerm) + private IList IndexAndSearch(IEnumerable packages, string searchTerm, string semVerLevel) { Directory d = new RAMDirectory(); @@ -614,6 +676,7 @@ private IList IndexAndSearch(IEnumerable packages, string sear Skip = 0, Take = 10, SearchTerm = searchTerm, + SemVerLevel = semVerLevel }; var results = luceneSearchService.Search(searchFilter).Result.Data.ToList(); diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj index 892a5e4323..38c0c45ede 100644 --- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj +++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj @@ -197,10 +197,10 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - ..\..\packages\NuGet.Common.4.3.0-preview1-2507\lib\net45\NuGet.Common.dll + ..\..\packages\NuGet.Common.4.3.0-preview1-2524\lib\net45\NuGet.Common.dll - ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2507\lib\net45\NuGet.Frameworks.dll + ..\..\packages\NuGet.Frameworks.4.3.0-preview1-2524\lib\net45\NuGet.Frameworks.dll False @@ -208,17 +208,17 @@ True - ..\..\packages\NuGet.Packaging.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.dll + ..\..\packages\NuGet.Packaging.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.dll - ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2507\lib\net45\NuGet.Packaging.Core.dll + ..\..\packages\NuGet.Packaging.Core.4.3.0-preview1-2524\lib\net45\NuGet.Packaging.Core.dll ..\..\packages\NuGet.Services.KeyVault.1.0.0.0\lib\net45\NuGet.Services.KeyVault.dll True - ..\..\packages\NuGet.Versioning.4.3.0-preview1-2507\lib\net45\NuGet.Versioning.dll + ..\..\packages\NuGet.Versioning.4.3.0-preview1-2524\lib\net45\NuGet.Versioning.dll False @@ -405,6 +405,8 @@ + + diff --git a/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs b/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs index 3bcc61bc79..7a01aa3840 100644 --- a/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs +++ b/tests/NuGetGallery.Facts/OData/Interceptors/PackageExtensionsFacts.cs @@ -12,8 +12,10 @@ public class PackageExtensionsFacts { public class TheProjectV2FeedPackageMethod { - [Fact] - public void MapsBasicPackagePropertiesCorrectly() + [Theory] + [InlineData(null)] // SemVerLevelKey.Unknown + [InlineData(2)] // SemVerLevelKey.SemVer2 + public void MapsBasicPackagePropertiesCorrectly(int? semVerLevelKey) { // Arrange var packages = new List @@ -25,7 +27,8 @@ public void MapsBasicPackagePropertiesCorrectly() var projected = PackageExtensions.ProjectV2FeedPackage( packages, siteRoot: "http://nuget.org", - includeLicenseReport: true).ToList(); + includeLicenseReport: true, + semVerLevelKey: semVerLevelKey).ToList(); // Assert var actual = projected.Single(); @@ -74,7 +77,8 @@ public void InjectsGalleryUrlsCorrectly() var projected = PackageExtensions.ProjectV2FeedPackage( packages, siteRoot: "http://nuget.org", - includeLicenseReport: true).ToList(); + includeLicenseReport: true, + semVerLevelKey: SemVerLevelKey.Unknown).ToList(); // Assert var actual = projected.Single(); @@ -97,7 +101,8 @@ public void InjectsDummyDateIfNotListed() var projected = PackageExtensions.ProjectV2FeedPackage( packages, siteRoot: "http://nuget.org", - includeLicenseReport: true).ToList(); + includeLicenseReport: true, + semVerLevelKey: SemVerLevelKey.Unknown).ToList(); // Assert var actual = projected.Single(); @@ -117,7 +122,8 @@ public void ReturnsNullLicenseReportInfoIfFeatureDisabled() var projected = PackageExtensions.ProjectV2FeedPackage( packages, siteRoot: "http://nuget.org", - includeLicenseReport: false).ToList(); + includeLicenseReport: false, + semVerLevelKey: SemVerLevelKey.Unknown).ToList(); // Assert var actual = projected.Single(); @@ -147,7 +153,9 @@ public static Package CreateFakeBasePackage() Description = "The standard repository for all knowledge and wisdom", IconUrl = "http://notreal.example/foo.ico", IsLatestStable = false, + IsLatestStableSemVer2 = false, IsLatest = true, + IsLatestSemVer2 = true, IsPrerelease = true, LastUpdated = new DateTime(2002, 4, 30), Language = "en-GB", diff --git a/tests/NuGetGallery.Facts/OData/Serializers/V1FeedPackageAnnotationStrategyFacts.cs b/tests/NuGetGallery.Facts/OData/Serializers/V1FeedPackageAnnotationStrategyFacts.cs new file mode 100644 index 0000000000..1a92250dc1 --- /dev/null +++ b/tests/NuGetGallery.Facts/OData/Serializers/V1FeedPackageAnnotationStrategyFacts.cs @@ -0,0 +1,136 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Data.OData; +using Microsoft.Data.OData.Atom; +using System; +using System.Linq; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Routing; +using System.Web.Mvc; +using Xunit; + +namespace NuGetGallery.OData.Serializers +{ + public class V1FeedPackageAnnotationStrategyFacts + { + private readonly string _contentType = "application/zip"; + + [Fact] + public void CanNotAnnotateNullObject() + { + // Arrange + var annotationStrategy = new V1FeedPackageAnnotationStrategy(_contentType); + + // Act + var canAnnotate = annotationStrategy.CanAnnotate(null); + + // Assert + Assert.False(canAnnotate); + } + + [Fact] + public void CanNotAnnotateV2FeedPackage() + { + // Arrange + var v2FeedPackage = new V2FeedPackage(); + var annotationStrategy = new V1FeedPackageAnnotationStrategy(_contentType); + + // Act + var canAnnotate = annotationStrategy.CanAnnotate(v2FeedPackage); + + // Assert + Assert.False(canAnnotate); + } + + [Fact] + public void SetsAtomEntryMetadataAnnotation() + { + // Arrange + var v1FeedPackage = new V1FeedPackage() + { + Id = "SomePackageId", + Version = "1.0.0", + Title = "Title", + Authors = ".NET Foundation", + LastUpdated = DateTime.UtcNow, + Summary = "Summary" + }; + var annotationStrategy = new V1FeedPackageAnnotationStrategy(_contentType); + var oDataEntry = new ODataEntry(); + var request = CreateHttpRequestMessage(); + + var expectedAtomEntryMetadataAnnotation = new AtomEntryMetadata() + { + Title = v1FeedPackage.Title, + Authors = new[] { new AtomPersonMetadata { Name = v1FeedPackage.Authors } }, + Updated = v1FeedPackage.LastUpdated, + Summary = v1FeedPackage.Summary + }; + + // Act + annotationStrategy.Annotate(request, oDataEntry, v1FeedPackage); + + var actualAtomEntryMetadataAnnotation = oDataEntry.GetAnnotation(); + + // Assert + Assert.Equal(expectedAtomEntryMetadataAnnotation.Title.Text, actualAtomEntryMetadataAnnotation.Title.Text); + Assert.Equal(expectedAtomEntryMetadataAnnotation.Summary.Text, actualAtomEntryMetadataAnnotation.Summary.Text); + Assert.Equal(expectedAtomEntryMetadataAnnotation.Authors.Single().Name, actualAtomEntryMetadataAnnotation.Authors.Single().Name); + Assert.Equal(expectedAtomEntryMetadataAnnotation.Updated, actualAtomEntryMetadataAnnotation.Updated); + } + + [Fact] + public void SetsMediaResourceAnnotation() + { + // Arrange + var v1FeedPackage = new V1FeedPackage() + { + Id = "SomePackageId", + Version = "1.0.0", + Title = "Title", + Authors = ".NET Foundation", + LastUpdated = DateTime.UtcNow, + Summary = "Summary" + }; + var annotationStrategy = new V1FeedPackageAnnotationStrategy(_contentType); + var oDataEntry = new ODataEntry(); + var request = CreateHttpRequestMessage(); + + // Act + annotationStrategy.Annotate(request, oDataEntry, v1FeedPackage); + + // Assert + Assert.Equal(_contentType, oDataEntry.MediaResource.ContentType); + Assert.Equal("https://localhost/api/v1/package/SomePackageId/1.0.0", oDataEntry.MediaResource.ReadLink.ToString()); + } + + private static HttpRequestMessage CreateHttpRequestMessage() + { + var downloadPackageRoute = new HttpRoute( + "api/v1/package/{id}/{version}", + defaults: new HttpRouteValueDictionary( + new + { + controller = "Api", + action = "GetPackageApi", + version = UrlParameter.Optional + }), + constraints: new HttpRouteValueDictionary( + new + { + httpMethod = new HttpMethodConstraint(HttpMethod.Get) + })); + + var routeCollection = new HttpRouteCollection(); + routeCollection.Add("v1" + RouteName.DownloadPackage, downloadPackageRoute); + + var httpConfiguration = new HttpConfiguration(routeCollection); + + var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost/api/v1/Packages"); + request.SetConfiguration(httpConfiguration); + return request; + } + } +} diff --git a/tests/NuGetGallery.Facts/OData/Serializers/V2FeedPackageAnnotationStrategyFacts.cs b/tests/NuGetGallery.Facts/OData/Serializers/V2FeedPackageAnnotationStrategyFacts.cs new file mode 100644 index 0000000000..ba58f4517d --- /dev/null +++ b/tests/NuGetGallery.Facts/OData/Serializers/V2FeedPackageAnnotationStrategyFacts.cs @@ -0,0 +1,175 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Data.OData; +using Microsoft.Data.OData.Atom; +using System; +using System.Linq; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Routing; +using System.Web.Mvc; +using Xunit; + +namespace NuGetGallery.OData.Serializers +{ + public class V2FeedPackageAnnotationStrategyFacts + { + private readonly string _contentType = "application/zip"; + + [Fact] + public void CanNotAnnotateNullObject() + { + // Arrange + var annotationStrategy = new V2FeedPackageAnnotationStrategy(_contentType); + + // Act + var canAnnotate = annotationStrategy.CanAnnotate(null); + + // Assert + Assert.False(canAnnotate); + } + + [Fact] + public void CanNotAnnotateV1FeedPackage() + { + // Arrange + var v1FeedPackage = new V1FeedPackage(); + var annotationStrategy = new V2FeedPackageAnnotationStrategy(_contentType); + + // Act + var canAnnotate = annotationStrategy.CanAnnotate(v1FeedPackage); + + // Assert + Assert.False(canAnnotate); + } + + [Fact] + public void SetsAtomEntryMetadataAnnotation() + { + // Arrange + var v2FeedPackage = new V2FeedPackage() + { + Id = "SomePackageId", + Version = "1.0.0", + Title = "Title", + Authors = ".NET Foundation", + LastUpdated = DateTime.UtcNow, + Summary = "Summary" + }; + var annotationStrategy = new V2FeedPackageAnnotationStrategy(_contentType); + var oDataEntry = new ODataEntry(); + var request = CreateHttpRequestMessage("https://localhost/api/v2/Packages"); + + var expectedAtomEntryMetadataAnnotation = new AtomEntryMetadata() + { + Title = v2FeedPackage.Id, + Authors = new[] { new AtomPersonMetadata { Name = v2FeedPackage.Authors } }, + Updated = v2FeedPackage.LastUpdated, + Summary = v2FeedPackage.Summary + }; + + // Act + annotationStrategy.Annotate(request, oDataEntry, v2FeedPackage); + + var actualAtomEntryMetadataAnnotation = oDataEntry.GetAnnotation(); + + // Assert + Assert.Equal(expectedAtomEntryMetadataAnnotation.Title.Text, actualAtomEntryMetadataAnnotation.Title.Text); + Assert.Equal(expectedAtomEntryMetadataAnnotation.Summary.Text, actualAtomEntryMetadataAnnotation.Summary.Text); + Assert.Equal(expectedAtomEntryMetadataAnnotation.Authors.Single().Name, actualAtomEntryMetadataAnnotation.Authors.Single().Name); + Assert.Equal(expectedAtomEntryMetadataAnnotation.Updated, actualAtomEntryMetadataAnnotation.Updated); + } + + [Theory] + [InlineData("https://localhost/api/v2/Packages")] + [InlineData("https://localhost/api/v2/Packages()")] + [InlineData("https://localhost/api/v2/Packages(Id='SomePackageId',Version='1.0.0')")] + [InlineData("https://localhost/api/v2/FindPackagesById()?id='SomePackageId'")] + [InlineData("https://localhost/api/v2/FindPackagesById(Id='SomePackageId')")] + [InlineData("https://localhost/api/v2/Search()?searchTerm='SomePackageId'")] + [InlineData("https://localhost/api/v2/GetUpdates()?packageIds=='SomePackageId'")] + public void NormalizesNavigationLinksWhenSet(string requestUri) + { + // Arrange + var v2FeedPackage = new V2FeedPackage() + { + Id = "SomePackageId", + Version = "1.0.0", + Title = "Title", + Authors = ".NET Foundation", + LastUpdated = DateTime.UtcNow, + Summary = "Summary" + }; + var annotationStrategy = new V2FeedPackageAnnotationStrategy(_contentType); + var oDataEntry = new ODataEntry(); + var dummyIdLink = new Uri("https://localhost"); + oDataEntry.Id = dummyIdLink.ToString(); + oDataEntry.EditLink = dummyIdLink; + oDataEntry.ReadLink = dummyIdLink; + + var request = CreateHttpRequestMessage(requestUri); + var expectedNormalizedLink = "https://localhost/api/v2/Packages(Id='SomePackageId',Version='1.0.0')"; + + // Act + annotationStrategy.Annotate(request, oDataEntry, v2FeedPackage); + + // Assert + Assert.Equal(expectedNormalizedLink, oDataEntry.ReadLink.ToString()); + Assert.Equal(expectedNormalizedLink, oDataEntry.EditLink.ToString()); + Assert.Equal(expectedNormalizedLink, oDataEntry.Id.ToString()); + } + + [Fact] + public void SetsMediaResourceAnnotation() + { + // Arrange + var v2FeedPackage = new V2FeedPackage() + { + Id = "SomePackageId", + Version = "1.0.0", + Title = "Title", + Authors = ".NET Foundation", + LastUpdated = DateTime.UtcNow, + Summary = "Summary" + }; + var annotationStrategy = new V2FeedPackageAnnotationStrategy(_contentType); + var oDataEntry = new ODataEntry(); + var request = CreateHttpRequestMessage("https://localhost/api/v2/Packages"); + + // Act + annotationStrategy.Annotate(request, oDataEntry, v2FeedPackage); + + // Assert + Assert.Equal(_contentType, oDataEntry.MediaResource.ContentType); + Assert.Equal("https://localhost/api/v2/package/SomePackageId/1.0.0", oDataEntry.MediaResource.ReadLink.ToString()); + } + + private static HttpRequestMessage CreateHttpRequestMessage(string requestUri) + { + var downloadPackageRoute = new HttpRoute( + "api/v2/package/{id}/{version}", + defaults: new HttpRouteValueDictionary( + new + { + controller = "Api", + action = "GetPackageApi", + version = UrlParameter.Optional + }), + constraints: new HttpRouteValueDictionary( + new + { + httpMethod = new HttpMethodConstraint(HttpMethod.Get) + })); + + var routeCollection = new HttpRouteCollection(); + routeCollection.Add("v2" + RouteName.DownloadPackage, downloadPackageRoute); + + var httpConfiguration = new HttpConfiguration(routeCollection); + + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.SetConfiguration(httpConfiguration); + return request; + } + } +} 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/Services/MessageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs index aef8174bde..171fec1a76 100644 --- a/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/MessageServiceFacts.cs @@ -14,6 +14,7 @@ using NuGetGallery.Framework; using NuGetGallery.Infrastructure.Authentication; using Xunit; +using NuGet.Versioning; namespace NuGetGallery { @@ -576,10 +577,17 @@ public void ApiKeyAddedMessageIsCorrect() public class TheSendPackageAddedNoticeMethod { - [Fact] - public void WillSendEmailToAllOwners() + [Theory] + [InlineData("1.2.3")] + [InlineData("1.2.3-alpha")] + [InlineData("1.2.3-alpha.1")] + [InlineData("1.2.3+metadata")] + [InlineData("1.2.3-alpha+metadata")] + [InlineData("1.2.3-alpha.1+metadata")] + public void WillSendEmailToAllOwners(string version) { // Arrange + var nugetVersion = new NuGetVersion(version); var packageRegistration = new PackageRegistration { Id = "smangit", @@ -591,14 +599,17 @@ public void WillSendEmailToAllOwners() }; var package = new Package { - Version = "1.2.3", + Version = version, PackageRegistration = packageRegistration }; packageRegistration.Packages.Add(package); // Act var messageService = new TestableMessageService(); - messageService.SendPackageAddedNotice(package, "http://dummy1", "http://dummy2", "http://dummy3"); + var packageUrl = $"https://localhost/packages/{packageRegistration.Id}/{nugetVersion.ToNormalizedString()}"; + var supportUrl = $"https://localhost/packages/{packageRegistration.Id}/{nugetVersion.ToNormalizedString()}/ReportMyPackage"; + var emailSettingsUrl = "https://localhost/account"; + messageService.SendPackageAddedNotice(package, packageUrl, supportUrl, emailSettingsUrl); // Assert var message = messageService.MockMailSender.Sent.Last(); @@ -606,9 +617,9 @@ public void WillSendEmailToAllOwners() Assert.Equal("yung@example.com", message.To[0].Address); Assert.Equal("flynt@example.com", message.To[1].Address); Assert.Equal(TestGalleryNoReplyAddress, message.From); - Assert.Contains("[Joe Shmoe] Package published - smangit 1.2.3", message.Subject); + Assert.Contains($"[Joe Shmoe] Package published - {packageRegistration.Id} {nugetVersion.ToNormalizedString()}", message.Subject); Assert.Contains( - "The package [smangit 1.2.3](http://dummy1) was just published on Joe Shmoe. If this was not intended, please [contact support](http://dummy2).", message.Body); + $"The package [{packageRegistration.Id} {nugetVersion.ToFullString()}]({packageUrl}) was just published on Joe Shmoe. If this was not intended, please [contact support]({supportUrl}).", message.Body); } [Fact] diff --git a/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs index 8c873bb53c..cde3879e8e 100644 --- a/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageFileServiceFacts.cs @@ -439,7 +439,7 @@ static string BuildFileName( return string.Format( Constants.PackageFileSavePathTemplate, id.ToLowerInvariant(), - NuGetVersionNormalizer.Normalize(version).ToLowerInvariant(), // No matter what ends up getting passed in, the version should be normalized + NuGetVersionFormatter.Normalize(version).ToLowerInvariant(), // No matter what ends up getting passed in, the version should be normalized Constants.NuGetPackageFileExtension); } diff --git a/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs index 80a30368e2..d377c8f95b 100644 --- a/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/PackageServiceFacts.cs @@ -8,7 +8,6 @@ using Moq; using NuGet.Frameworks; using NuGet.Packaging; -using NuGet.Packaging.Core; using NuGet.Versioning; using NuGetGallery.Auditing; using NuGetGallery.Framework; @@ -174,7 +173,7 @@ public async Task RemovesRelatedPendingOwnerRequest() repository.VerifyAll(); } - + [Fact] public async Task WritesAnAuditRecord() { @@ -497,7 +496,7 @@ public async Task UpdateIndexIfCommitChangesIsTrue() var currentUser = new User(); // Act - var package = await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), currentUser, commitChanges: true); + var package = await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), currentUser, commitChanges: true); // Assert indexingService.Verify(); @@ -628,10 +627,10 @@ private async Task WillSaveTheCreatedPackageWhenThePackageRegistrationAlreadyExi { var currentUser = new User(); var packageRegistration = new PackageRegistration - { - Id = "theId", - Owners = new HashSet { currentUser }, - }; + { + Id = "theId", + Owners = new HashSet { currentUser }, + }; var packageRegistrationRepository = new Mock>(); var service = CreateService(packageRegistrationRepository: packageRegistrationRepository, setup: mockPackageService => { mockPackageService.Setup(x => x.FindPackageRegistrationById(It.IsAny())).Returns(packageRegistration); }); @@ -648,10 +647,10 @@ private async Task WillThrowIfThePackageRegistrationAlreadyExistsAndTheCurrentUs { var currentUser = new User(); var packageRegistration = new PackageRegistration - { - Id = "theId", - Owners = new HashSet() - }; + { + Id = "theId", + Owners = new HashSet() + }; var packageRegistrationRepository = new Mock>(); var service = CreateService(packageRegistrationRepository: packageRegistrationRepository, setup: mockPackageService => { mockPackageService.Setup(x => x.FindPackageRegistrationById(It.IsAny())).Returns(packageRegistration); }); @@ -702,25 +701,21 @@ private async Task WillThrowIfTheNuGetPackageIdIsLongerThanMaxPackageIdLength() } [Fact] - private async Task WillThrowIfTheNuGetPackageSpecialVersionContainsADot() + private async Task DoesNotThrowIfTheNuGetPackageSpecialVersionContainsADot() { var service = CreateService(); var nugetPackage = CreateNuGetPackage(id: "theId", version: "1.2.3-alpha.0"); - var ex = await Assert.ThrowsAsync(async () => await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null)); - - Assert.Equal(String.Format(Strings.NuGetPackageReleaseVersionWithDot, "Version"), ex.Message); + await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null); } [Fact] - private async Task WillThrowIfTheNuGetPackageSpecialVersionContainsOnlyNumbers() + private async Task DoesNotThrowIfTheNuGetPackageSpecialVersionContainsOnlyNumbers() { var service = CreateService(); var nugetPackage = CreateNuGetPackage(id: "theId", version: "1.2.3-12345"); - var ex = await Assert.ThrowsAsync(async () => await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null)); - - Assert.Equal(String.Format(Strings.NuGetPackageReleaseVersionContainsOnlyNumerics, "Version"), ex.Message); + await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null); } [Fact] @@ -738,7 +733,7 @@ private async Task WillThrowIfTheNuGetPackageAuthorsIsLongerThan4000() private async Task WillThrowIfTheNuGetPackageCopyrightIsLongerThan4000() { var service = CreateService(); - var nugetPackage = CreateNuGetPackage(copyright: "theCopyright".PadRight(4001, '_')); + var nugetPackage = CreateNuGetPackage(copyright: "theCopyright".PadRight(4001, '_')); var ex = await Assert.ThrowsAsync(async () => await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null)); @@ -842,7 +837,7 @@ public async Task WillThrowIfTheNuGetPackageDescriptionIsNull() private async Task WillThrowIfTheNuGetPackageDescriptionIsLongerThan4000() { var service = CreateService(); - var nugetPackage = CreateNuGetPackage(description: "theDescription".PadRight(4001, '_')); + var nugetPackage = CreateNuGetPackage(description: "theDescription".PadRight(4001, '_')); var ex = await Assert.ThrowsAsync(async () => await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null)); @@ -897,7 +892,7 @@ private async Task WillThrowIfTheNuGetPackageSummaryIsLongerThan4000() private async Task WillThrowIfTheNuGetPackageTagsIsLongerThan4000() { var service = CreateService(); - var nugetPackage = CreateNuGetPackage(tags: "theTags".PadRight(4001, '_')); + var nugetPackage = CreateNuGetPackage(tags: "theTags".PadRight(4001, '_')); var ex = await Assert.ThrowsAsync(async () => await service.CreatePackageAsync(nugetPackage.Object, new PackageStreamMetadata(), null)); @@ -951,7 +946,7 @@ private async Task WillSaveSupportedFrameworks() Assert.Equal("net40", package.SupportedFrameworks.First().TargetFramework); Assert.Equal("net35", package.SupportedFrameworks.ElementAt(1).TargetFramework); } - + [Fact] private async Task WillNotSaveAnySupportedFrameworksWhenThereIsAnAnyTargetFramework() { @@ -1030,8 +1025,7 @@ public async Task DoNotCommitIfCommitChangesIsFalse() packageRegistration.Packages.Add(package); var packageRepository = new Mock>(); - var service = CreateService(packageRepository: packageRepository, setup: - mockService => { mockService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + var service = CreateService(packageRepository: packageRepository); // Act await service.UpdateIsLatestAsync(packageRegistration, commitChanges: false); @@ -1049,8 +1043,7 @@ public async Task CommitIfCommitChangesIsTrue() packageRegistration.Packages.Add(package); var packageRepository = new Mock>(); - var service = CreateService(packageRepository: packageRepository, setup: - mockService => { mockService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + var service = CreateService(packageRepository: packageRepository); // Act await service.UpdateIsLatestAsync(packageRegistration, true); @@ -1060,7 +1053,51 @@ public async Task CommitIfCommitChangesIsTrue() } [Fact] - public async Task WillUpdateIsLatest1() + public async Task ResetsCurrentLatestPackageVersionsBeforeUpdate() + { + // Arrange + var packageRegistration = new PackageRegistration(); + + var previousLatestStable = new Package { PackageRegistration = packageRegistration, Version = "1.0.0", IsLatestStable = true }; + packageRegistration.Packages.Add(previousLatestStable); + var previousLatest = new Package { PackageRegistration = packageRegistration, Version = "1.0.0-alpha", IsLatest = true, IsPrerelease = true }; + packageRegistration.Packages.Add(previousLatest); + var previousLatestStableSemVer2 = new Package { PackageRegistration = packageRegistration, Version = "1.0.1+metadata", IsLatestStableSemVer2 = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + packageRegistration.Packages.Add(previousLatestStableSemVer2); + var previousLatestSemVer2 = new Package { PackageRegistration = packageRegistration, Version = "1.0.1-alpha.1", IsLatestSemVer2 = true, IsPrerelease = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + packageRegistration.Packages.Add(previousLatestSemVer2); + + // Simulates adding newer versions, to ensure the previous latest are no longer latest at end of test. + var newLatestStable = new Package { PackageRegistration = packageRegistration, Version = "1.0.1", IsLatestStable = true }; + packageRegistration.Packages.Add(newLatestStable); + var newLatest = new Package { PackageRegistration = packageRegistration, Version = "1.0.2-alpha", IsLatest = true, IsPrerelease = true }; + packageRegistration.Packages.Add(newLatest); + var newLatestStableSemVer2 = new Package { PackageRegistration = packageRegistration, Version = "1.0.2+metadata", IsLatestStableSemVer2 = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + packageRegistration.Packages.Add(newLatestStableSemVer2); + var newLatestSemVer2 = new Package { PackageRegistration = packageRegistration, Version = "1.0.3-alpha.1", IsLatestSemVer2 = true, IsPrerelease = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + packageRegistration.Packages.Add(newLatestSemVer2); + + var packageRepository = new Mock>(); + + var service = CreateService(packageRepository: packageRepository); + + // Act + await service.UpdateIsLatestAsync(packageRegistration, commitChanges: true); + + // Assert + Assert.False(previousLatestStable.IsLatestStable); + Assert.False(previousLatest.IsLatest); + Assert.False(previousLatestSemVer2.IsLatestSemVer2); + Assert.False(previousLatestStableSemVer2.IsLatestStableSemVer2); + + Assert.True(newLatestStable.IsLatestStable); + Assert.True(newLatest.IsLatest); + Assert.True(newLatestSemVer2.IsLatestSemVer2); + Assert.True(newLatestStableSemVer2.IsLatestStableSemVer2); + } + + [Fact] + public async Task UpdateIsLatestScenarioForPrereleaseAsAbsoluteLatest() { // Arrange var packages = new HashSet(); @@ -1072,22 +1109,27 @@ public async Task WillUpdateIsLatest1() var packageRepository = new Mock>(MockBehavior.Strict); packageRepository.Setup(r => r.CommitChangesAsync()) .Returns(Task.CompletedTask).Verifiable(); - var service = CreateService(packageRepository: packageRepository, setup: - mockService => { mockService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package10A); }); + var service = CreateService(packageRepository: packageRepository); // Act await service.UpdateIsLatestAsync(packageRegistration, true); // Assert Assert.True(package10A.IsLatest); + Assert.True(package10A.IsLatestSemVer2); Assert.False(package10A.IsLatestStable); + Assert.False(package10A.IsLatestStableSemVer2); + Assert.False(package09.IsLatest); + Assert.False(package09.IsLatestSemVer2); Assert.True(package09.IsLatestStable); + Assert.True(package09.IsLatestStableSemVer2); + packageRepository.Verify(); } [Fact] - public async Task WillUpdateIsLatest2() + public async Task UpdateIsLatestScenarioForStableAsAbsoluteLatest() { // Arrange var packages = new HashSet(); @@ -1101,19 +1143,119 @@ public async Task WillUpdateIsLatest2() var packageRepository = new Mock>(MockBehavior.Strict); packageRepository.Setup(r => r.CommitChangesAsync()) .Returns(Task.CompletedTask).Verifiable(); - var service = CreateService(packageRepository: packageRepository, setup: - mockService => { mockService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package100); }); + var service = CreateService(packageRepository: packageRepository); // Act await service.UpdateIsLatestAsync(packageRegistration, true); // Assert Assert.True(package100.IsLatest); + Assert.True(package100.IsLatestSemVer2); Assert.True(package100.IsLatestStable); + Assert.True(package100.IsLatestStableSemVer2); + Assert.False(package10A.IsLatest); + Assert.False(package10A.IsLatestSemVer2); Assert.False(package10A.IsLatestStable); + Assert.False(package10A.IsLatestStableSemVer2); + Assert.False(package09.IsLatest); + Assert.False(package09.IsLatestSemVer2); Assert.False(package09.IsLatestStable); + Assert.False(package09.IsLatestStableSemVer2); + + packageRepository.Verify(); + } + + [Fact] + public async Task UpdateIsLatestScenarioForSemVer2PrereleaseAsAbsoluteLatest() + { + // Arrange + var packages = new HashSet(); + var packageRegistration = new PackageRegistration { Packages = packages }; + var semVer2Package = new Package { PackageRegistration = packageRegistration, Version = "1.0.1-alpha.1", IsPrerelease = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + packages.Add(semVer2Package); + var package100 = new Package { PackageRegistration = packageRegistration, Version = "1.0.0" }; + packages.Add(package100); + var package10A = new Package { PackageRegistration = packageRegistration, Version = "1.0.0-a", IsPrerelease = true }; + packages.Add(package10A); + var package09 = new Package { PackageRegistration = packageRegistration, Version = "0.9.0" }; + packages.Add(package09); + var packageRepository = new Mock>(MockBehavior.Strict); + packageRepository.Setup(r => r.CommitChangesAsync()) + .Returns(Task.CompletedTask).Verifiable(); + var service = CreateService(packageRepository: packageRepository); + + // Act + await service.UpdateIsLatestAsync(packageRegistration, true); + + // Assert + Assert.True(semVer2Package.IsLatestSemVer2); + Assert.False(semVer2Package.IsLatestStableSemVer2); + Assert.False(semVer2Package.IsLatest); + Assert.False(semVer2Package.IsLatestStable); + + Assert.True(package100.IsLatest); + Assert.False(package100.IsLatestSemVer2); + Assert.True(package100.IsLatestStable); + Assert.True(package100.IsLatestStableSemVer2); + + Assert.False(package10A.IsLatest); + Assert.False(package10A.IsLatestSemVer2); + Assert.False(package10A.IsLatestStable); + Assert.False(package10A.IsLatestStableSemVer2); + + Assert.False(package09.IsLatest); + Assert.False(package09.IsLatestSemVer2); + Assert.False(package09.IsLatestStable); + Assert.False(package09.IsLatestStableSemVer2); + + packageRepository.Verify(); + } + + [Fact] + public async Task UpdateIsLatestScenarioForSemVer2StableAsAbsoluteLatest() + { + // Arrange + var packages = new HashSet(); + var packageRegistration = new PackageRegistration { Packages = packages }; + var semVer2Package = new Package { PackageRegistration = packageRegistration, Version = "1.0.1+metadata", SemVerLevelKey = SemVerLevelKey.SemVer2 }; + packages.Add(semVer2Package); + var package100 = new Package { PackageRegistration = packageRegistration, Version = "1.0.0" }; + packages.Add(package100); + var package10A = new Package { PackageRegistration = packageRegistration, Version = "1.0.0-a", IsPrerelease = true }; + packages.Add(package10A); + var package09 = new Package { PackageRegistration = packageRegistration, Version = "0.9.0" }; + packages.Add(package09); + var packageRepository = new Mock>(MockBehavior.Strict); + packageRepository.Setup(r => r.CommitChangesAsync()) + .Returns(Task.CompletedTask).Verifiable(); + var service = CreateService(packageRepository: packageRepository); + + // Act + await service.UpdateIsLatestAsync(packageRegistration, true); + + // Assert + Assert.True(semVer2Package.IsLatestSemVer2); + Assert.True(semVer2Package.IsLatestStableSemVer2); + Assert.False(semVer2Package.IsLatest); + Assert.False(semVer2Package.IsLatestStable); + + Assert.True(package100.IsLatest); + Assert.False(package100.IsLatestSemVer2); + Assert.True(package100.IsLatestStable); + Assert.False(package100.IsLatestStableSemVer2); + + Assert.False(package10A.IsLatest); + Assert.False(package10A.IsLatestSemVer2); + Assert.False(package10A.IsLatestStable); + Assert.False(package10A.IsLatestStableSemVer2); + + Assert.False(package09.IsLatest); + Assert.False(package09.IsLatestSemVer2); + Assert.False(package09.IsLatestStable); + Assert.False(package09.IsLatestStableSemVer2); + packageRepository.Verify(); } } @@ -1146,13 +1288,15 @@ public void WillThrowIfIdIsNullOrEmpty(string id) Assert.Equal("id", ex.ParamName); } - [Fact] - public void ReturnsTheLatestStableVersionIfAvailable() + [Theory] + [InlineData(null)] + [InlineData("2.0.0")] + public void ReturnsTheLatestStableVersionIfAvailable(string semVerLevel) { // Arrange var repository = new Mock>(MockBehavior.Strict); var packageRegistration = new PackageRegistration { Id = "theId" }; - var package1 = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatestStable = true }; + var package1 = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatestStable = true, IsLatestStableSemVer2 = true }; var package2 = new Package { Version = "1.0.0a", PackageRegistration = packageRegistration, IsPrerelease = true, Listed = true, IsLatest = true }; repository @@ -1161,13 +1305,36 @@ public void ReturnsTheLatestStableVersionIfAvailable() var service = CreateService(packageRepository: repository); // Act - var result = service.FindPackageByIdAndVersion("theId", version: null); + var result = service.FindPackageByIdAndVersion("theId", version: null, semVerLevelKey: SemVerLevelKey.ForSemVerLevel(semVerLevel)); // Assert Assert.NotNull(result); Assert.Equal("1.0", result.Version); } + [Fact] + public void ReturnsTheLatestStableSemVer2VersionIfAvailable() + { + // Arrange + var repository = new Mock>(MockBehavior.Strict); + var packageRegistration = new PackageRegistration { Id = "theId" }; + var package0 = new Package { Version = "1.0.0+metadata", PackageRegistration = packageRegistration, Listed = true, IsLatestStableSemVer2 = true }; + var package1 = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatestStable = true }; + var package2 = new Package { Version = "1.0.0a", PackageRegistration = packageRegistration, IsPrerelease = true, Listed = true, IsLatest = true }; + + repository + .Setup(repo => repo.GetAll()) + .Returns(new[] { package0, package1, package2 }.AsQueryable()); + var service = CreateService(packageRepository: repository); + + // Act + var result = service.FindPackageByIdAndVersion("theId", version: null, semVerLevelKey: SemVerLevelKey.SemVer2); + + // Assert + Assert.NotNull(result); + Assert.Equal("1.0.0+metadata", result.Version); + } + [Fact] public void ReturnsTheLatestVersionIfNoLatestStableVersionIsAvailable() { @@ -1237,7 +1404,7 @@ public void ReturnsTheMostRecentVersionIfNoLatestVersionIsAvailable() public class TheFindAbsoluteLatestPackageByIdMethod { [Fact] - public void ReturnsTheLatestVersion() + public void ReturnsTheLatestVersionWhenSemVerLevelUnknown() { // Arrange var repository = new Mock>(MockBehavior.Strict); @@ -1251,7 +1418,7 @@ public void ReturnsTheLatestVersion() var service = CreateService(packageRepository: repository); // Act - var result = service.FindAbsoluteLatestPackageById("theId"); + var result = service.FindAbsoluteLatestPackageById("theId", SemVerLevelKey.Unknown); // Assert Assert.NotNull(result); @@ -1259,14 +1426,36 @@ public void ReturnsTheLatestVersion() } [Fact] - public void ReturnsTheMostRecentVersion() + public void ReturnsTheLatestVersionWhenSemVerLevel2() + { + // Arrange + var repository = new Mock>(MockBehavior.Strict); + var packageRegistration = new PackageRegistration { Id = "theId" }; + var package1 = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatestStable = true }; + var package2 = new Package { Version = "2.0.0-alpha.1", PackageRegistration = packageRegistration, IsPrerelease = true, Listed = true, IsLatest = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + + repository + .Setup(repo => repo.GetAll()) + .Returns(new[] { package1, package2 }.AsQueryable()); + var service = CreateService(packageRepository: repository); + + // Act + var result = service.FindAbsoluteLatestPackageById("theId", SemVerLevelKey.SemVer2); + + // Assert + Assert.NotNull(result); + Assert.Equal("2.0.0-alpha.1", result.Version); + } + + [Fact] + public void ReturnsTheMostRecentVersionWhenSemVerLevelUnknown() { // Arrange var repository = new Mock>(MockBehavior.Strict); var packageRegistration = new PackageRegistration { Id = "theId" }; var package1 = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true }; - var package2 = new Package { Version = "2.0.0a", PackageRegistration = packageRegistration, IsPrerelease = true, Listed = true }; - var package3 = new Package { Version = "2.0.0", PackageRegistration = packageRegistration, Listed = true }; + var package2 = new Package { Version = "2.0.0-alpha", PackageRegistration = packageRegistration, IsPrerelease = true, Listed = true }; + var package3 = new Package { Version = "2.0.0", PackageRegistration = packageRegistration, Listed = true, IsLatest = true }; repository .Setup(repo => repo.GetAll()) @@ -1274,11 +1463,34 @@ public void ReturnsTheMostRecentVersion() var service = CreateService(packageRepository: repository); // Act - var result = service.FindAbsoluteLatestPackageById("theId"); + var result = service.FindAbsoluteLatestPackageById("theId", SemVerLevelKey.Unknown); // Assert Assert.NotNull(result); - Assert.Equal("2.0.0a", result.Version); + Assert.Equal("2.0.0", result.Version); + } + + [Fact] + public void ReturnsTheMostRecentVersionWhenSemVerLevel2() + { + // Arrange + var repository = new Mock>(MockBehavior.Strict); + var packageRegistration = new PackageRegistration { Id = "theId" }; + var package1 = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true }; + var package2 = new Package { Version = "2.0.0-alpha.1", PackageRegistration = packageRegistration, IsPrerelease = true, Listed = true, SemVerLevelKey = SemVerLevelKey.SemVer2 }; + var package3 = new Package { Version = "2.0.0+metadata", PackageRegistration = packageRegistration, Listed = true, SemVerLevelKey = SemVerLevelKey.SemVer2, IsLatestSemVer2 = true }; + + repository + .Setup(repo => repo.GetAll()) + .Returns(new[] { package1, package2, package3 }.AsQueryable()); + var service = CreateService(packageRepository: repository); + + // Act + var result = service.FindAbsoluteLatestPackageById("theId", SemVerLevelKey.SemVer2); + + // Assert + Assert.NotNull(result); + Assert.Equal("2.0.0+metadata", result.Version); } } @@ -1289,7 +1501,7 @@ public void ReturnsAListedPackage() { var owner = new User { Username = "someone" }; var packageRegistration = new PackageRegistration { Id = "theId", Owners = { owner } }; - var package = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatest = true, IsLatestStable = true }; + var package = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatestSemVer2 = true, IsLatestStableSemVer2 = true }; packageRegistration.Packages.Add(package); var context = GetFakeContext(); @@ -1344,8 +1556,8 @@ public void ReturnsAPackageForEachPackageRegistration() var owner = new User { Username = "someone" }; var packageRegistrationA = new PackageRegistration { Id = "idA", Owners = { owner } }; var packageRegistrationB = new PackageRegistration { Id = "idB", Owners = { owner } }; - var packageA = new Package { Version = "1.0", PackageRegistration = packageRegistrationA, Listed = true, IsLatest = true, IsLatestStable = true }; - var packageB = new Package { Version = "1.0", PackageRegistration = packageRegistrationB, Listed = true, IsLatest = true, IsLatestStable = true }; + var packageA = new Package { Version = "1.0", PackageRegistration = packageRegistrationA, Listed = true, IsLatestSemVer2 = true, IsLatestStableSemVer2 = true }; + var packageB = new Package { Version = "1.0", PackageRegistration = packageRegistrationB, Listed = true, IsLatestSemVer2 = true, IsLatestStableSemVer2 = true }; packageRegistrationA.Packages.Add(packageA); packageRegistrationB.Packages.Add(packageB); @@ -1364,7 +1576,30 @@ public void ReturnsAPackageForEachPackageRegistration() } [Fact] - public void ReturnsOnlyLatestStablePackageIfBothExist() + public void ReturnsOnlyLatestStableSemVer2PackageIfBothExist() + { + var owner = new User { Username = "someone" }; + var packageRegistration = new PackageRegistration { Id = "theId", Owners = { owner } }; + var latestPackage = new Package { Version = "2.0.0-alpha", PackageRegistration = packageRegistration, Listed = true, IsLatest = true }; + var latestSemVer2Package = new Package { Version = "2.0.0-alpha.1", PackageRegistration = packageRegistration, Listed = true, IsLatestSemVer2 = true }; + var latestStablePackage = new Package { Version = "1.0", PackageRegistration = packageRegistration, Listed = true, IsLatestStableSemVer2 = true }; + packageRegistration.Packages.Add(latestPackage); + packageRegistration.Packages.Add(latestStablePackage); + + var context = GetFakeContext(); + context.Users.Add(owner); + context.PackageRegistrations.Add(packageRegistration); + context.Packages.Add(latestPackage); + context.Packages.Add(latestStablePackage); + var service = Get(); + + var packages = service.FindPackagesByOwner(owner, includeUnlisted: false).ToList(); + Assert.Equal(1, packages.Count); + Assert.Contains(latestStablePackage, packages); + } + + [Fact] + public void ReturnsOnlyLatestStablePackageIfNoLatestStableSemVer2Exist() { var owner = new User { Username = "someone" }; var packageRegistration = new PackageRegistration { Id = "theId", Owners = { owner } }; @@ -1489,12 +1724,12 @@ public async Task WritesAnAuditRecord() var packageRepository = new Mock>(); var auditingService = new TestAuditingService(); var service = CreateService( - packageRepository: packageRepository, + packageRepository: packageRepository, auditingService: auditingService); // Act await service.MarkPackageListedAsync(package); - + // Assert Assert.True(auditingService.WroteRecord(ar => ar.Action == AuditedPackageAction.List @@ -1617,18 +1852,18 @@ public class ThePublishPackageMethod public async Task WillSetThePublishedDateOnThePackageBeingPublished() { var package = new Package + { + Version = "1.0.42", + PackageRegistration = new PackageRegistration { - Version = "1.0.42", - PackageRegistration = new PackageRegistration - { - Id = "theId", - Packages = new HashSet() - } - }; + Id = "theId", + Packages = new HashSet() + } + }; package.PackageRegistration.Packages.Add(package); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync("theId", "1.0.42"); @@ -1651,7 +1886,7 @@ public async Task WillSetThePublishedDateOnThePackageBeingPublishedWithOverload( package.PackageRegistration.Packages.Add(package); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync(package, commitChanges: false); @@ -1663,19 +1898,19 @@ public async Task WillSetThePublishedDateOnThePackageBeingPublishedWithOverload( public async Task WillSetUpdateIsLatestStableOnThePackageWhenItIsTheLatestVersionWithOverload() { var package = new Package + { + Version = "1.0.42", + PackageRegistration = new PackageRegistration { - Version = "1.0.42", - PackageRegistration = new PackageRegistration - { - Id = "theId", - Packages = new HashSet() - } - }; + Id = "theId", + Packages = new HashSet() + } + }; package.PackageRegistration.Packages.Add(package); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync(package); @@ -1698,7 +1933,7 @@ public async Task WillSetUpdateIsLatestStableOnThePackageWhenItIsTheLatestVersio package.PackageRegistration.Packages.Add(new Package { Version = "1.0", PackageRegistration = package.PackageRegistration }); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync("theId", "1.0.42"); @@ -1709,25 +1944,25 @@ public async Task WillSetUpdateIsLatestStableOnThePackageWhenItIsTheLatestVersio public async Task WillNotSetUpdateIsLatestStableOnThePackageWhenItIsNotTheLatestVersionWithOverload() { var package = new Package + { + Version = "1.0.42", + PackageRegistration = new PackageRegistration { - Version = "1.0.42", - PackageRegistration = new PackageRegistration - { - Id = "theId", - Packages = new HashSet() - } - }; + Id = "theId", + Packages = new HashSet() + } + }; package.PackageRegistration.Packages.Add(package); package.PackageRegistration.Packages.Add( new Package - { - Version = "2.0", - PackageRegistration = package.PackageRegistration, - Published = DateTime.UtcNow - }); + { + Version = "2.0", + PackageRegistration = package.PackageRegistration, + Published = DateTime.UtcNow + }); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync(package); @@ -1738,27 +1973,27 @@ public async Task WillNotSetUpdateIsLatestStableOnThePackageWhenItIsNotTheLatest public async Task PublishPackageUpdatesIsAbsoluteLatestForPrereleasePackage() { var package = new Package + { + Version = "1.0.42-alpha", + Published = DateTime.UtcNow, + PackageRegistration = new PackageRegistration { - Version = "1.0.42-alpha", - Published = DateTime.UtcNow, - PackageRegistration = new PackageRegistration - { - Id = "theId", - Packages = new HashSet() - }, - IsPrerelease = true, - }; + Id = "theId", + Packages = new HashSet() + }, + IsPrerelease = true, + }; package.PackageRegistration.Packages.Add(package); var package39 = new Package - { - Version = "1.0.39", - PackageRegistration = package.PackageRegistration, - Published = DateTime.UtcNow.AddDays(-1) - }; + { + Version = "1.0.39", + PackageRegistration = package.PackageRegistration, + Published = DateTime.UtcNow.AddDays(-1) + }; package.PackageRegistration.Packages.Add(package39); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync("theId", "1.0.42-alpha"); Assert.True(package39.IsLatestStable); @@ -1791,7 +2026,7 @@ public async Task PublishPackageUpdatesIsAbsoluteLatestForPrereleasePackageWithO package.PackageRegistration.Packages.Add(package39); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync(package); @@ -1805,28 +2040,28 @@ public async Task PublishPackageUpdatesIsAbsoluteLatestForPrereleasePackageWithO public async Task SetUpdateDoesNotSetIsLatestStableForAnyIfAllPackagesArePrerelease() { var package = new Package + { + Version = "1.0.42-alpha", + Published = DateTime.UtcNow, + IsPrerelease = true, + PackageRegistration = new PackageRegistration { - Version = "1.0.42-alpha", - Published = DateTime.UtcNow, - IsPrerelease = true, - PackageRegistration = new PackageRegistration - { - Id = "theId", - Packages = new HashSet() - } - }; + Id = "theId", + Packages = new HashSet() + } + }; package.PackageRegistration.Packages.Add(package); var package39 = new Package - { - Version = "1.0.39-beta", - PackageRegistration = package.PackageRegistration, - Published = DateTime.UtcNow.AddDays(-1), - IsPrerelease = true - }; + { + Version = "1.0.39-beta", + PackageRegistration = package.PackageRegistration, + Published = DateTime.UtcNow.AddDays(-1), + IsPrerelease = true + }; package.PackageRegistration.Packages.Add(package39); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync("theId", "1.0.42-alpha"); Assert.False(package39.IsLatestStable); @@ -1860,7 +2095,7 @@ public async Task SetUpdateDoesNotSetIsLatestStableForAnyIfAllPackagesArePrerele package.PackageRegistration.Packages.Add(package39); var packageRepository = new Mock>(); var service = CreateService(packageRepository: packageRepository, setup: - mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns(package); }); + mockPackageService => { mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns(package); }); await service.PublishPackageAsync(package); Assert.False(package39.IsLatestStable); @@ -1875,7 +2110,7 @@ public async Task WillThrowIfThePackageDoesNotExist() var service = CreateService(setup: mockPackageService => { - mockPackageService.Setup(x => x.FindPackageByIdAndVersion(It.IsAny(), It.IsAny(), true)).Returns( + mockPackageService.Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny())).Returns( (Package)null); }); @@ -1923,11 +2158,11 @@ await Assert.ThrowsAsync( public async Task RemovesPendingPackageOwner() { var packageOwnerRequest = new PackageOwnerRequest - { - PackageRegistrationKey = 1, - RequestingOwnerKey = 99, - NewOwnerKey = 200 - }; + { + PackageRegistrationKey = 1, + RequestingOwnerKey = 99, + NewOwnerKey = 200 + }; var packageOwnerRequestRepository = new Mock>(); packageOwnerRequestRepository.Setup(r => r.GetAll()).Returns(new[] { packageOwnerRequest }.AsQueryable()); packageOwnerRequestRepository.Setup(r => r.DeleteOnCommit(packageOwnerRequest)).Verifiable(); diff --git a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs index f3c8128f87..74c6ba244d 100644 --- a/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs +++ b/tests/NuGetGallery.Facts/Services/ReflowPackageServiceFacts.cs @@ -299,7 +299,7 @@ private static Mock SetupPackageService(Package package) packageService.CallBase = true; packageService - .Setup(s => s.FindPackageByIdAndVersion("test", "1.0.0", true)) + .Setup(s => s.FindPackageByIdAndVersionStrict("test", "1.0.0")) .Returns(package) .Verifiable(); 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 diff --git a/tests/NuGetGallery.Facts/UrlExtensionsFacts.cs b/tests/NuGetGallery.Facts/UrlExtensionsFacts.cs index 5513b4fe6c..e650049cd0 100644 --- a/tests/NuGetGallery.Facts/UrlExtensionsFacts.cs +++ b/tests/NuGetGallery.Facts/UrlExtensionsFacts.cs @@ -1,6 +1,7 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Web.Mvc; using Xunit; namespace NuGetGallery @@ -23,5 +24,27 @@ public void PropagatesNull() Assert.Null(fixedUrl); } } + + public class ThePackageHelperMethod + { + [Fact] + public void UsesNormalizedVersionInUrls() + { + var package = new Package + { + PackageRegistration = new PackageRegistration + { + Id = "TestPackageId" + }, + NormalizedVersion = "1.0.0-alpha.1", + Version = "1.0.0-alpha.1+metadata" + }; + + string fixedUrl = UrlExtensions.Package(TestUtility.MockUrlHelper(), package); + + Assert.DoesNotContain("metadata", fixedUrl); + Assert.EndsWith(package.NormalizedVersion, fixedUrl); + } + } } } diff --git a/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs b/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs index bc5d7d3a74..b496a8e89a 100644 --- a/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs +++ b/tests/NuGetGallery.Facts/ViewModels/ListPackageItemViewModelFacts.cs @@ -231,5 +231,31 @@ public void UseVersionIfLatestAndStableNotSame() listPackageItemViewModel.LatestStableVersion = true; Assert.False(listPackageItemViewModel.UseVersion); } + + [Fact] + public void UseVersionIfLatestSemVer2AndStableSemVer2NotSame() + { + var package = new Package() + { + SemVerLevelKey = SemVerLevelKey.SemVer2, + IsLatestSemVer2 = true, + IsLatestStableSemVer2 = false + }; + + var listPackageItemViewModel = new ListPackageItemViewModel(package); + Assert.True(listPackageItemViewModel.UseVersion); + + listPackageItemViewModel.LatestVersionSemVer2 = false; + listPackageItemViewModel.LatestStableVersionSemVer2 = true; + Assert.True(listPackageItemViewModel.UseVersion); + + listPackageItemViewModel.LatestVersionSemVer2 = false; + listPackageItemViewModel.LatestStableVersionSemVer2 = false; + Assert.True(listPackageItemViewModel.UseVersion); + + listPackageItemViewModel.LatestVersionSemVer2 = true; + listPackageItemViewModel.LatestStableVersionSemVer2 = true; + Assert.False(listPackageItemViewModel.UseVersion); + } } } diff --git a/tests/NuGetGallery.Facts/packages.config b/tests/NuGetGallery.Facts/packages.config index 43e1c54671..b628017d6e 100644 --- a/tests/NuGetGallery.Facts/packages.config +++ b/tests/NuGetGallery.Facts/packages.config @@ -38,13 +38,13 @@ - - + + - - + + - + diff --git a/tests/NuGetGallery.LoadTests/LoadTests.cs b/tests/NuGetGallery.LoadTests/LoadTests.cs index 856ff6228d..886b17870b 100644 --- a/tests/NuGetGallery.LoadTests/LoadTests.cs +++ b/tests/NuGetGallery.LoadTests/LoadTests.cs @@ -78,7 +78,7 @@ public async Task FindPackagesByIdForPredefinedPackage() public async Task FindPackagesBySpecificIdAndVersion() { string packageId = "Microsoft.Web.Infrastructure"; - string version = "1.0.0.0"; + string version = "1.0.0"; string url = UrlHelper.V2FeedRootUrl + @"Packages(Id='" + packageId + "',Version='" + version + "')"; string expectedText = @"" + UrlHelper.V2FeedRootUrl + "Packages(Id='" + packageId + "',Version='" + version + "')"; var odataHelper = new ODataHelper();