From 9ba36a06ea872ce62cbdf301578cf48ebbc06bbc Mon Sep 17 00:00:00 2001 From: Joel Verhagen Date: Fri, 9 Jun 2017 09:38:49 -0700 Subject: [PATCH] [Redesign] Merge dev into feature-redesign (#4078) * Fixed Report Abuse Page's Accessibility (#4001) Fixes #4002. Relevant to [VSTS #395879](https://devdiv.visualstudio.com/DefaultCollection/DevDiv/_workitems?id=395879&_a=edit). This help-text bug was for the sign-in page, but this also affects the Report Abuse page. * Fixed accessibility of email field in report abuse page * Fixed signature accessibility * Use ServerCommon's Response Code Processor (#3999) * Added Nuget.Services.Logging; moved to TelemetryResponseCodeProcessor * Added binding redirect for AI * Removing the "WITH(ONLINE=ON)" from the index creation as per #3952 (#4004) * Removing the "WITH(ONLINE=ON)" from the index creation as per #3952 (#4004) (#4030) * Remove validation rules that block uploading valid semver2 versions #3645 (#3757) * Add support for semVerLevel query parameter to V2 endpoints (#3714) * adding new optional semVerLevel query parameter to v2 odata endpoints * adding new optional semVerLevel query parameter to v2 autocomplete endpoints * Applying semVerLevel filter on v2 OData endpoints * Use [FromUri] attribute on semVerLevel (avoids having single quotes in the parameter value) * Ensure navigation links on v2 feeds use normalized version * Clarifying comment on Get(Id=,Version=) v2 API * Properly default to semver2 inclusion on Get(Id=,Version=) * Compare NormalizedVersion to be able to retrieve matching SemVer2 package versions for a given normalized version string. * Code review feedback * Update and fix broken test data * Keep legacy version compliance checks in place for non-SemVer2 versions (#3761) * Keep legacy version compatibility checks in place for non-semver2 versions * Added comment to clarify the reasons behind the legacy version check. * Fix typo * Rename test for clarity * code review feedback * Set SemVerLevelKey after setting Dependencies * #3861 V2 NuGetEntityTypeSerializer Id link patcher must retain curated feed name (#3864) * Support Is(Absolute)Latest for SemVer2 + semVerLevel for SearchService (#3842) * LuceneIndexingService in Gallery should take into account IsLatest(Stable)SemVer2 (#3863) * POST VerifyPackage version validation should use ToFullString comparison * ODataV2CuratedFeedController should support semver2 by default when requesting a specific version, and compare on NormalizedVersion * Refactor NuGetEntityTypeSerializer + unit test coverage (#3879) * Use NormalizedVersion in URLs contained in PackageAddedNotice (#3886) * Add nullcheck and use TryParse. (#3890) * Show full version on package details page (#3887) * Highlight semver2 packages on package details view (#3893) * Fix bug in IsLatest(Stable)SemVer2 (#3895) * VerifyPackage on ApiController should treat version as optional parameter (#3903) * UpdateIsLatest not resetting IsLatest(Stable)SemVer2 on previous latest versions (#3909) * Fix malformed URL in redirect after package upload (#3915) * Minor fix for search results package URLs (when to use version or not in the URL) * UrlHelper extension for Package should use NormalizedVersion (#3925) * Default to latest stable semver2 on package details page (#3930) * User profile page does not show SemVer 2.0.0 packages #3911 (#3933) * Fix functional test failure SearchMicrosoftDotNetCuratedFeed #3941 (#3942) * Fix System.NotSupportedException on User profile page (#3943) * Fix Functional Test failure for ODataFeeds.V2FeedExtendedTests.FindPackagesByIdTest #3947 (#3948) * Fix load test failure due to incorrect test setup (#3957) * Hijack IsLatest(Stable)Version OData filter when semVerLevel=2.0.0 (#3966) * Detect if package only differ by metadata and show optimal user-facing error message (#3970) * Update Semver2 package details message with final nuget client version #3897 (#3988) * On package validation failure an actionable error message should be displayed. #3916 (#4031) * Make downloads link on home page a proper link (#4052) * Fix the date format on stats page (#4057) * Update telemetry processors (#4059) * Reorder SemVer2Latest migration to match deployment history (#4062) * Average download shown incorrectly when its 1.x #4039 (#4040) * Average download shown incorrectly when its 1.x #4039 * Moved logic to viewmodel and added UTs * Port latest changes made to package details page * SemVer2 - Missing db index on Packages table #498 (#4073) * SemVer2 - Missing db index on Packages table #498 * SemVer2 - Missing db index on Packages table for partial search #499 (#4074) * Package-Versions autocomplete endpoint does not properly handle semVerLevel when using the db #4086 (#4087) * v2 package-versions auto-complete endpoint should exclude deleted versions #4092 (#4093) * Remove auto-refresh AJAX call for total stats on home page #4090 (#4091) * Fix merge conflicts --- .../Client/SearchClient.cs | 8 +- .../Auditing/PackageAuditRecord.cs | 2 +- src/NuGetGallery.Core/CoreStrings.Designer.cs | 18 +- src/NuGetGallery.Core/CoreStrings.resx | 6 +- src/NuGetGallery.Core/Entities/Package.cs | 3 + .../NuGetGallery.Core.csproj | 10 +- .../NuGetVersionExtensions.cs | 26 +- .../Packaging/ManifestValidator.cs | 43 +- src/NuGetGallery.Core/SemVerLevelKey.cs | 65 ++- src/NuGetGallery.Core/packages.config | 10 +- .../App_Start/NuGetODataV2FeedConfig.cs | 2 + src/NuGetGallery/App_Start/OwinStartup.cs | 16 +- .../Admin/Controllers/DeleteController.cs | 2 +- src/NuGetGallery/Controllers/ApiController.cs | 37 +- .../Controllers/CuratedFeedsController.cs | 12 +- .../Controllers/ODataV1FeedController.cs | 17 +- .../ODataV2CuratedFeedController.cs | 116 +++-- .../Controllers/ODataV2FeedController.cs | 197 ++++++-- .../Controllers/PackagesController.cs | 95 +++- .../Lucene/ExternalSearchService.cs | 3 +- .../Lucene/LuceneIndexingService.cs | 7 +- .../Lucene/LuceneSearchService.cs | 23 +- .../Infrastructure/PackageIndexEntity.cs | 4 +- .../201705031714183_AddIndexSemVerLevelKey.cs | 2 +- ...AddSemVer2LatestVersionColumns.Designer.cs | 29 ++ ...61829243_AddSemVer2LatestVersionColumns.cs | 20 + ...829243_AddSemVer2LatestVersionColumns.resx | 126 +++++ ...632469_AddIndexIsLatestSemVer2.Designer.cs | 29 ++ ...201706080632469_AddIndexIsLatestSemVer2.cs | 17 + ...1706080632469_AddIndexIsLatestSemVer2.resx | 126 +++++ ...LevelKeyPackageRegistrationKey.Designer.cs | 29 ++ ...dexSemVerLevelKeyPackageRegistrationKey.cs | 18 + ...xSemVerLevelKeyPackageRegistrationKey.resx | 126 +++++ src/NuGetGallery/NuGetGallery.csproj | 52 +- src/NuGetGallery/OData/PackageExtensions.cs | 33 +- .../OData/QueryAllowed/ODataQueryFilter.cs | 3 + .../NormalizeVersionInterceptor.cs | 2 +- .../OData/SearchService/SearchAdaptor.cs | 20 +- .../OData/SearchService/SearchHijacker.cs | 2 +- .../FeedPackageAnnotationStrategy.cs | 57 +++ .../IFeedPackageAnnotationStrategy.cs | 14 + .../Serializers/NuGetEntityTypeSerializer.cs | 102 +--- .../V1FeedPackageAnnotationStrategy.cs | 56 +++ .../V2FeedPackageAnnotationStrategy.cs | 107 ++++ .../AutoCompleteDatabasePackageIdsQuery.cs | 27 +- ...utoCompleteDatabasePackageVersionsQuery.cs | 23 +- .../Queries/AutoCompleteDatabaseQuery.cs | 2 +- .../Queries/AutoCompleteServiceQuery.cs | 13 +- .../AutocompleteServicePackageIdsQuery.cs | 5 +- ...AutocompleteServicePackageVersionsQuery.cs | 5 +- .../Queries/IAutoCompletePackageIdsQuery.cs | 3 +- .../IAutoCompletePackageVersionsQuery.cs | 3 +- .../RequestModels/VerifyPackageRequest.cs | 10 + src/NuGetGallery/Scripts/stats.js | 2 - ...nloadCountObjectMaterializedInterceptor.cs | 2 +- src/NuGetGallery/Services/IPackageService.cs | 22 +- .../Services/PackageFileService.cs | 2 +- src/NuGetGallery/Services/PackageService.cs | 308 ++++++++---- .../Services/ReflowPackageService.cs | 2 +- src/NuGetGallery/Services/SearchFilter.cs | 2 + src/NuGetGallery/Strings.Designer.cs | 29 +- src/NuGetGallery/Strings.resx | 9 +- src/NuGetGallery/Telemetry/Telemetry.cs | 11 +- .../Telemetry/TelemetryResponseCodeFilter.cs | 35 -- src/NuGetGallery/UrlExtensions.cs | 2 +- .../ViewModels/DisplayPackageViewModel.cs | 4 + .../ViewModels/ListPackageItemViewModel.cs | 3 +- .../ViewModels/PackageViewModel.cs | 17 +- .../Views/Packages/DisplayPackage.cshtml | 22 +- .../Views/Packages/VerifyPackage.cshtml | 7 +- .../Views/Shared/_ListPackage.cshtml | 2 +- .../Views/Statistics/_PivotTable.cshtml | 2 +- src/NuGetGallery/Web.config | 4 + src/NuGetGallery/WebApi/QueryResult.cs | 39 +- .../WebApi/QueryResultDefaults.cs | 18 + src/NuGetGallery/packages.config | 14 +- .../NuGetGallery.Core.Facts.csproj | 10 +- .../Packaging/ManifestValidatorFacts.cs | 33 +- .../SemVerLevelKeyFacts.cs | 111 ++++ tests/NuGetGallery.Core.Facts/packages.config | 10 +- .../Controllers/ApiControllerFacts.cs | 30 +- .../ODataFeedControllerFactsBase.cs | 25 +- .../Controllers/ODataV1FeedControllerFacts.cs | 18 +- .../ODataV2CuratedFeedControllerFacts.cs | 238 ++++++++- .../Controllers/ODataV2FeedControllerFacts.cs | 320 +++++++++++- .../Controllers/PackagesControllerFacts.cs | 61 ++- .../LuceneSearchServiceFacts.cs | 153 ++++-- .../NuGetGallery.Facts.csproj | 12 +- .../Interceptors/PackageExtensionsFacts.cs | 20 +- .../V1FeedPackageAnnotationStrategyFacts.cs | 136 +++++ .../V2FeedPackageAnnotationStrategyFacts.cs | 175 +++++++ .../Services/FeedServiceFacts.cs | 4 +- .../Services/MessageServiceFacts.cs | 23 +- .../Services/PackageFileServiceFacts.cs | 2 +- .../Services/PackageServiceFacts.cs | 475 +++++++++++++----- .../Services/ReflowPackageServiceFacts.cs | 2 +- .../Infrastructure/FeedServiceHelpers.cs | 7 + .../NuGetGallery.Facts/UrlExtensionsFacts.cs | 23 + .../DisplayPackageViewModelFacts.cs | 64 +++ .../ListPackageItemViewModelFacts.cs | 26 + tests/NuGetGallery.Facts/packages.config | 10 +- tests/NuGetGallery.LoadTests/LoadTests.cs | 2 +- 102 files changed, 3526 insertions(+), 805 deletions(-) create mode 100644 src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.Designer.cs create mode 100644 src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.cs create mode 100644 src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.resx create mode 100644 src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.Designer.cs create mode 100644 src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.cs create mode 100644 src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.resx create mode 100644 src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.Designer.cs create mode 100644 src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.cs create mode 100644 src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.resx create mode 100644 src/NuGetGallery/OData/Serializers/FeedPackageAnnotationStrategy.cs create mode 100644 src/NuGetGallery/OData/Serializers/IFeedPackageAnnotationStrategy.cs create mode 100644 src/NuGetGallery/OData/Serializers/V1FeedPackageAnnotationStrategy.cs create mode 100644 src/NuGetGallery/OData/Serializers/V2FeedPackageAnnotationStrategy.cs delete mode 100644 src/NuGetGallery/Telemetry/TelemetryResponseCodeFilter.cs create mode 100644 src/NuGetGallery/WebApi/QueryResultDefaults.cs create mode 100644 tests/NuGetGallery.Facts/OData/Serializers/V1FeedPackageAnnotationStrategyFacts.cs create mode 100644 tests/NuGetGallery.Facts/OData/Serializers/V2FeedPackageAnnotationStrategyFacts.cs 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/App_Start/OwinStartup.cs b/src/NuGetGallery/App_Start/OwinStartup.cs index f03d16652c..eb35010f5a 100644 --- a/src/NuGetGallery/App_Start/OwinStartup.cs +++ b/src/NuGetGallery/App_Start/OwinStartup.cs @@ -16,6 +16,7 @@ using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; +using NuGet.Services.Logging; using NuGetGallery.Authentication; using NuGetGallery.Authentication.Providers; using NuGetGallery.Authentication.Providers.Cookie; @@ -64,8 +65,19 @@ public static void Configuration(IAppBuilder app) var telemetryProcessorChainBuilder = TelemetryConfiguration.Active.TelemetryProcessorChainBuilder; - // Add filters - telemetryProcessorChainBuilder.Use(next => new TelemetryResponseCodeFilter(next)); + // Add processors + telemetryProcessorChainBuilder.Use(next => + { + var processor = new RequestTelemetryProcessor(next); + + processor.SuccessfulResponseCodes.Add(400); + processor.SuccessfulResponseCodes.Add(404); + + return processor; + }); + + telemetryProcessorChainBuilder.Use( + next => new ExceptionTelemetryProcessor(next, Telemetry.TelemetryClient)); // Note: sampling rate must be a factor 100/N where N is a whole number // e.g.: 50 (= 100/2), 33.33 (= 100/3), 25 (= 100/4), ... 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/201705031714183_AddIndexSemVerLevelKey.cs b/src/NuGetGallery/Migrations/201705031714183_AddIndexSemVerLevelKey.cs index a67af53471..dfc8374991 100644 --- a/src/NuGetGallery/Migrations/201705031714183_AddIndexSemVerLevelKey.cs +++ b/src/NuGetGallery/Migrations/201705031714183_AddIndexSemVerLevelKey.cs @@ -7,7 +7,7 @@ public partial class AddIndexSemVerLevelKey : DbMigration { public override void Up() { - Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Packages_SemVerLevelKey' AND object_id = OBJECT_ID('Packages')) CREATE NONCLUSTERED INDEX [nci_wi_Packages_SemVerLevelKey] ON [dbo].[Packages] ([SemVerLevelKey], [IsLatest], [Deleted]) INCLUDE ([PackageRegistrationKey]) WITH (ONLINE = ON)"); + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Packages_SemVerLevelKey' AND object_id = OBJECT_ID('Packages')) CREATE NONCLUSTERED INDEX [nci_wi_Packages_SemVerLevelKey] ON [dbo].[Packages] ([SemVerLevelKey], [IsLatest], [Deleted]) INCLUDE ([PackageRegistrationKey])"); } public override void Down() diff --git a/src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.Designer.cs b/src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.Designer.cs new file mode 100644 index 0000000000..0dfe190b75 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706061829243_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 "201706061829243_AddSemVer2LatestVersionColumns"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.cs b/src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.cs new file mode 100644 index 0000000000..3f64a552f8 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706061829243_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/201706061829243_AddSemVer2LatestVersionColumns.resx b/src/NuGetGallery/Migrations/201706061829243_AddSemVer2LatestVersionColumns.resx new file mode 100644 index 0000000000..bb860d09b2 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706061829243_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/Migrations/201706080632469_AddIndexIsLatestSemVer2.Designer.cs b/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.Designer.cs new file mode 100644 index 0000000000..a29d7179e8 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.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 AddIndexIsLatestSemVer2 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexIsLatestSemVer2)); + + string IMigrationMetadata.Id + { + get { return "201706080632469_AddIndexIsLatestSemVer2"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.cs b/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.cs new file mode 100644 index 0000000000..4c42ac1361 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.cs @@ -0,0 +1,17 @@ +using System.Data.Entity.Migrations; + +namespace NuGetGallery.Migrations +{ + public partial class AddIndexIsLatestSemVer2 : DbMigration + { + public override void Up() + { + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Packages_IsLatestSemVer2' AND object_id = OBJECT_ID('Packages')) CREATE NONCLUSTERED INDEX [nci_wi_Packages_IsLatestSemVer2] ON [dbo].[Packages] ([SemVerLevelKey],[Deleted]) INCLUDE ([IsLatest],[IsLatestSemVer2])"); + } + + public override void Down() + { + Sql("DROP INDEX [nci_wi_Packages_IsLatestSemVer2] ON [dbo].[Packages]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.resx b/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.resx new file mode 100644 index 0000000000..2ba16b9c01 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706080632469_AddIndexIsLatestSemVer2.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/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.Designer.cs b/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.Designer.cs new file mode 100644 index 0000000000..8924b3403e --- /dev/null +++ b/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.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 AddIndexSemVerLevelKeyPackageRegistrationKey : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddIndexSemVerLevelKeyPackageRegistrationKey)); + + string IMigrationMetadata.Id + { + get { return "201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.cs b/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.cs new file mode 100644 index 0000000000..323187c277 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddIndexSemVerLevelKeyPackageRegistrationKey : DbMigration + { + public override void Up() + { + Sql("IF NOT EXISTS(SELECT * FROM sys.indexes WHERE name = 'nci_wi_Packages_SemVerLevelKey_PackageRegistrationKey' AND object_id = OBJECT_ID('Packages')) CREATE NONCLUSTERED INDEX [nci_wi_Packages_SemVerLevelKey_PackageRegistrationKey] ON [dbo].[Packages] ([SemVerLevelKey],[IsPrerelease],[Deleted]) INCLUDE ([PackageRegistrationKey],[Description],[Tags])"); + } + + public override void Down() + { + Sql("DROP INDEX [nci_wi_Packages_SemVerLevelKey_PackageRegistrationKey] ON [dbo].[Packages]"); + } + } +} diff --git a/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.resx b/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.resx new file mode 100644 index 0000000000..2ba16b9c01 --- /dev/null +++ b/src/NuGetGallery/Migrations/201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.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 c7fe24170b..bb3ca0353c 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -425,36 +425,35 @@ ..\..\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 - - - False - ..\..\packages\NuGet.Logging.3.5.0-beta-1160\lib\net45\NuGet.Logging.dll - True + ..\..\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 True + + ..\..\packages\NuGet.Services.Logging.2.2.3\lib\net452\NuGet.Services.Logging.dll + True + False ..\..\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,8 +761,24 @@ 201705041614287_UserSecurityPolicies_SubscriptionColumn.cs + + + 201706061829243_AddSemVer2LatestVersionColumns.cs + + + + 201706080632469_AddIndexIsLatestSemVer2.cs + + + + 201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.cs + + + + + @@ -775,7 +790,6 @@ - @@ -1470,6 +1484,7 @@ + PreserveNewest @@ -1733,6 +1748,15 @@ 201705041614287_UserSecurityPolicies_SubscriptionColumn.cs + + 201706061829243_AddSemVer2LatestVersionColumns.cs + + + 201706080632469_AddIndexIsLatestSemVer2.cs + + + 201706080930506_AddIndexSemVerLevelKeyPackageRegistrationKey.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..df0f4f62e7 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 p.[Deleted] <> 1 AND {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] IS NULL OR 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/Scripts/stats.js b/src/NuGetGallery/Scripts/stats.js index e44c47e2e9..7b206d0d56 100644 --- a/src/NuGetGallery/Scripts/stats.js +++ b/src/NuGetGallery/Scripts/stats.js @@ -12,8 +12,6 @@ // var section = $('section.aggstatserr'); // section.show(); }); - - setTimeout(function () { getStats(currData); }, 30000); } function update(data, currData, key) { 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 dba56754da..df123c5146 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 96b7517036..cf01e2cb68 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/Telemetry/Telemetry.cs b/src/NuGetGallery/Telemetry/Telemetry.cs index 605a4cf605..08ace7c533 100644 --- a/src/NuGetGallery/Telemetry/Telemetry.cs +++ b/src/NuGetGallery/Telemetry/Telemetry.cs @@ -9,13 +9,18 @@ namespace NuGetGallery { internal static class Telemetry { - private static readonly TelemetryClient _telemetryClient = new TelemetryClient(); + static Telemetry() + { + TelemetryClient = new TelemetryClient(); + } + + internal static TelemetryClient TelemetryClient { get; } public static void TrackEvent(string eventName, IDictionary properties = null, IDictionary metrics = null) { try { - _telemetryClient.TrackEvent(eventName, properties, metrics); + TelemetryClient.TrackEvent(eventName, properties, metrics); } catch { @@ -27,7 +32,7 @@ public static void TrackException(Exception exception, IDictionary pack TotalDaysSinceCreated = 0; DownloadsPerDay = 0; } + + DownloadsPerDayLabel = DownloadsPerDay < 1 ? "<1" : DownloadsPerDay.ToNuGetNumberString(); } public void SetPendingMetadata(PackageEdit pendingMetadata) @@ -84,5 +86,7 @@ public bool HasNewerPrerelease } public bool? IsIndexed { get; set; } + + public string DownloadsPerDayLabel { get; private set; } } } \ No newline at end of file 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 85a2645b46..012b4162fd 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -42,7 +42,7 @@

@Model.Title.Abbreviate(40) - @Model.Version + @Model.FullVersion

@foreach (var line in Model.Description.ToStringSafe().Split('\n')) @@ -87,7 +87,7 @@ ) } - @if (Model.LatestStableVersion && Model.HasNewerPrerelease) + @if (Model.LatestStableVersionSemVer2 && Model.HasNewerPrerelease) { @ViewHelpers.AlertInfo( @ @@ -117,6 +117,16 @@ ) } + @if (Model.IsSemVer2) + { + @ViewHelpers.AlertWarning( + @ + 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. + + ) + } + @if (!Model.Listed && !Model.Deleted) { if ((Model.IsOwner(User) || User.IsAdministrator())) @@ -257,7 +267,7 @@ @if (packageVersion.IsCurrent(Model)) { - @packageVersion.Version + @packageVersion.FullVersion @if (packageVersion.IsCurrent(Model)) { @:(current version) @@ -267,7 +277,7 @@ else { - @packageVersion.Version + @packageVersion.FullVersion } @@ -295,7 +305,7 @@ - @packageVersion.Version (deleted) + @packageVersion.FullVersion (deleted) @packageVersion.DownloadCount @@ -433,7 +443,7 @@
  • - @Model.DownloadsPerDay.ToNuGetNumberString() @(Model.DownloadsPerDay == 1 ? "download" : "downloads") + @Model.DownloadsPerDayLabel @(Model.DownloadsPerDayLabel == "1" || Model.DownloadsPerDayLabel == "<1" ? "download" : "downloads") per day (avg)
  • 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/Shared/_ListPackage.cshtml b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml index e574f8e40e..940fc593d0 100644 --- a/src/NuGetGallery/Views/Shared/_ListPackage.cshtml +++ b/src/NuGetGallery/Views/Shared/_ListPackage.cshtml @@ -28,7 +28,7 @@ - Latest version: @Model.Version @(Model.Prerelease ? "(prerelease)" : "") + Latest version: @Model.FullVersion @(Model.Prerelease ? "(prerelease)" : "") diff --git a/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml b/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml index 9ac299af15..ee465c53d6 100644 --- a/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml +++ b/src/NuGetGallery/Views/Statistics/_PivotTable.cshtml @@ -11,7 +11,7 @@
      - +
      diff --git a/src/NuGetGallery/Web.config b/src/NuGetGallery/Web.config index 5816b78c90..b640eed66f 100644 --- a/src/NuGetGallery/Web.config +++ b/src/NuGetGallery/Web.config @@ -436,6 +436,10 @@ + + + + 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 994516b946..237f2e9997 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/DisplayPackageViewModelFacts.cs b/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.cs index 822a1118a8..9d8edaa31a 100644 --- a/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.cs +++ b/tests/NuGetGallery.Facts/ViewModels/DisplayPackageViewModelFacts.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.Linq; using NuGet.Versioning; using Xunit; @@ -44,5 +45,68 @@ public void TheCtorSortsPackageVersionsProperly() Assert.Equal("1.0.2", packageVersions[1].Version); Assert.Equal("1.0.10", packageVersions[0].Version); } + + [Fact] + public void DownloadsPerDayLabelShowsLessThanOneWhenAverageBelowOne() + { + // Arrange + const int downloadCount = 10; + const int daysSinceCreated = 11; + + var package = new Package + { + Dependencies = Enumerable.Empty().ToList(), + DownloadCount = downloadCount, + PackageRegistration = new PackageRegistration + { + Owners = Enumerable.Empty().ToList(), + DownloadCount = downloadCount + }, + Created = DateTime.UtcNow.AddDays(-daysSinceCreated), + Version = "1.0.10" + }; + + package.PackageRegistration.Packages = new[] { package }; + + var viewModel = new DisplayPackageViewModel(package, package.PackageRegistration.Packages.OrderByDescending(p => new NuGetVersion(p.Version))); + + // Act + var label = viewModel.DownloadsPerDayLabel; + + // Assert + Assert.Equal("<1", label); + } + + [Theory] + [InlineData(10, 10)] + [InlineData(11, 10)] + [InlineData(14, 10)] + [InlineData(15, 10)] + public void DownloadsPerDayLabelShowsOneWhenAverageBetweenOneAndOnePointFive(int downloadCount, int daysSinceCreated) + { + // Arrange + var package = new Package + { + Dependencies = Enumerable.Empty().ToList(), + DownloadCount = downloadCount, + PackageRegistration = new PackageRegistration + { + Owners = Enumerable.Empty().ToList(), + DownloadCount = downloadCount + }, + Created = DateTime.UtcNow.AddDays(-daysSinceCreated), + Version = "1.0.10" + }; + + package.PackageRegistration.Packages = new[] { package }; + + var viewModel = new DisplayPackageViewModel(package, package.PackageRegistration.Packages.OrderByDescending(p => new NuGetVersion(p.Version))); + + // Act + var label = viewModel.DownloadsPerDayLabel; + + // Assert + Assert.Equal("1", label); + } } } \ No newline at end of file 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();