diff --git a/src/Catalog/context/Registration.json b/src/Catalog/context/Registration.json index f26745c86..ba7144b8b 100644 --- a/src/Catalog/context/Registration.json +++ b/src/Catalog/context/Registration.json @@ -12,6 +12,7 @@ "parent" : { "@id" : "catalog:parent", "@type" : "@id" }, "tags": { "@container" : "@set", "@id": "tag" }, + "reasons" : { "@container" : "@set" }, "packageTargetFrameworks": { "@container": "@set", "@id": "packageTargetFramework" }, diff --git a/src/Catalog/sparql/ConstructCatalogEntryGraph.rq b/src/Catalog/sparql/ConstructCatalogEntryGraph.rq index 6bad34249..c508d45ca 100644 --- a/src/Catalog/sparql/ConstructCatalogEntryGraph.rq +++ b/src/Catalog/sparql/ConstructCatalogEntryGraph.rq @@ -21,7 +21,8 @@ CONSTRUCT nuget:language ?language ; nuget:authors ?authors ; nuget:tag ?tag ; - nuget:minClientVersion ?minClientVersion . + nuget:minClientVersion ?minClientVersion ; + nuget:deprecation ?deprecation . ?dependency_group a nuget:PackageDependencyGroup ; nuget:dependency ?dependency ; @@ -31,6 +32,15 @@ CONSTRUCT nuget:id ?dependency_id ; nuget:range ?dependency_range ; nuget:version ?dependency_version . + + ?deprecation a nuget:deprecation ; + nuget:reasons ?deprecation_reasons ; + nuget:message ?deprecation_message ; + nuget:alternatePackage ?deprecation_alternatePackage . + + ?deprecation_alternatePackage a nuget:alternatePackage ; + nuget:id ?deprecation_alternatePackage_id ; + nuget:range ?deprecation_alternatePackage_range . } WHERE { @@ -67,4 +77,17 @@ WHERE OPTIONAL { ?dependency nuget:version ?dependency_version . } } } + + OPTIONAL + { + ?catalogEntry nuget:deprecation ?deprecation . + ?deprecation nuget:reasons ?deprecation_reasons . + OPTIONAL { ?deprecation nuget:message ?deprecation_message . } + OPTIONAL + { + ?deprecation nuget:alternatePackage ?deprecation_alternatePackage . + ?deprecation_alternatePackage nuget:id ?deprecation_alternatePackage_id . + ?deprecation_alternatePackage nuget:range ?deprecation_alternatePackage_range . + } + } } diff --git a/src/Catalog/sparql/ConstructRegistrationPageContentGraph.rq b/src/Catalog/sparql/ConstructRegistrationPageContentGraph.rq index aee0ac04a..841430bc5 100644 --- a/src/Catalog/sparql/ConstructRegistrationPageContentGraph.rq +++ b/src/Catalog/sparql/ConstructRegistrationPageContentGraph.rq @@ -26,10 +26,11 @@ CONSTRUCT nuget:language ?language ; nuget:authors ?authors ; nuget:tag ?tag ; - nuget:minClientVersion ?minClientVersion . + nuget:minClientVersion ?minClientVersion ; + nuget:deprecation ?deprecation . ?dependency_group a nuget:PackageDependencyGroup ; - nuget:dependency ?dependency ; + nuget:dependency ?dependency ; nuget:targetFramework ?dependency_group_targetFramework . ?dependency a nuget:PackageDependency ; @@ -37,6 +38,15 @@ CONSTRUCT nuget:registration ?dependency_registration ; nuget:range ?dependency_range ; nuget:version ?dependency_version . + + ?deprecation a nuget:deprecation ; + nuget:reasons ?deprecation_reasons ; + nuget:message ?deprecation_message ; + nuget:alternatePackage ?deprecation_alternatePackage . + + ?deprecation_alternatePackage a nuget:alternatePackage ; + nuget:id ?deprecation_alternatePackage_id ; + nuget:range ?deprecation_alternatePackage_range . } WHERE { @@ -98,4 +108,17 @@ WHERE OPTIONAL { ?dependency nuget:version ?dependency_version . } } } + + OPTIONAL + { + ?catalogEntry nuget:deprecation ?deprecation . + ?deprecation nuget:reasons ?deprecation_reasons . + OPTIONAL { ?deprecation nuget:message ?deprecation_message . } + OPTIONAL + { + ?deprecation nuget:alternatePackage ?deprecation_alternatePackage . + ?deprecation_alternatePackage nuget:id ?deprecation_alternatePackage_id . + ?deprecation_alternatePackage nuget:range ?deprecation_alternatePackage_range . + } + } } diff --git a/tests/CatalogTests/CatalogTests.csproj b/tests/CatalogTests/CatalogTests.csproj index 7dc828779..1cf95568f 100644 --- a/tests/CatalogTests/CatalogTests.csproj +++ b/tests/CatalogTests/CatalogTests.csproj @@ -83,6 +83,8 @@ + + diff --git a/tests/CatalogTests/Helpers/CatalogIndependentPackageDetails.cs b/tests/CatalogTests/Helpers/CatalogIndependentPackageDetails.cs index fe432ca7d..662804d18 100644 --- a/tests/CatalogTests/Helpers/CatalogIndependentPackageDetails.cs +++ b/tests/CatalogTests/Helpers/CatalogIndependentPackageDetails.cs @@ -63,6 +63,8 @@ internal sealed class CatalogIndependentPackageDetails internal string CommitTimeStamp { get; } [JsonProperty(CatalogConstants.Created)] internal string Created { get; } + [JsonProperty(CatalogConstants.Deprecation)] + internal RegistrationPackageDeprecation Deprecation { get; } [JsonProperty(CatalogConstants.Description)] internal string Description { get; } [JsonProperty(CatalogConstants.Id)] @@ -97,7 +99,8 @@ internal CatalogIndependentPackageDetails( string version = null, string baseUri = null, string commitId = null, - DateTimeOffset? commitTimeStamp = null) + DateTimeOffset? commitTimeStamp = null, + RegistrationPackageDeprecation deprecation = null) { var utc = commitTimeStamp ?? DateTimeOffset.UtcNow; @@ -116,6 +119,7 @@ internal CatalogIndependentPackageDetails( CommitId = commitId ?? Guid.NewGuid().ToString("D"); CommitTimeStamp = utc.ToString(CatalogConstants.CommitTimeStampFormat); Created = utc.AddHours(-2).ToString(CatalogConstants.DateTimeFormat); + Deprecation = deprecation; Description = TestUtility.CreateRandomAlphanumericString(); LastEdited = utc.AddHours(-1).ToString(CatalogConstants.DateTimeFormat); Listed = true; diff --git a/tests/CatalogTests/Helpers/RegistrationPackageDeprecationAlternatePackage.cs b/tests/CatalogTests/Helpers/RegistrationPackageDeprecationAlternatePackage.cs new file mode 100644 index 000000000..e3ce467cf --- /dev/null +++ b/tests/CatalogTests/Helpers/RegistrationPackageDeprecationAlternatePackage.cs @@ -0,0 +1,26 @@ +// 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 Newtonsoft.Json; +using NgTests; + +namespace CatalogTests.Helpers +{ + public class RegistrationPackageDeprecationAlternatePackage + { + [JsonConstructor] + public RegistrationPackageDeprecationAlternatePackage( + string id, + string range) + { + Id = id; + Range = range; + } + + [JsonProperty(CatalogConstants.Id)] + public string Id { get; } + + [JsonProperty(CatalogConstants.Range)] + public string Range { get; } + } +} diff --git a/tests/CatalogTests/Helpers/RegistrationPackageDeprecationDetails.cs b/tests/CatalogTests/Helpers/RegistrationPackageDeprecationDetails.cs new file mode 100644 index 000000000..f4ca7328a --- /dev/null +++ b/tests/CatalogTests/Helpers/RegistrationPackageDeprecationDetails.cs @@ -0,0 +1,31 @@ +// 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 Newtonsoft.Json; +using NgTests; + +namespace CatalogTests.Helpers +{ + public class RegistrationPackageDeprecation + { + [JsonConstructor] + public RegistrationPackageDeprecation( + string[] reasons, + string message = null, + RegistrationPackageDeprecationAlternatePackage alternatePackage = null) + { + Reasons = reasons; + Message = message; + AlternatePackage = alternatePackage; + } + + [JsonProperty(CatalogConstants.Reasons)] + public string[] Reasons { get; } + + [JsonProperty(CatalogConstants.Message)] + public string Message { get; } + + [JsonProperty(CatalogConstants.AlternatePackage)] + public RegistrationPackageDeprecationAlternatePackage AlternatePackage { get; } + } +} diff --git a/tests/CatalogTests/Helpers/RegistrationPackageDetails.cs b/tests/CatalogTests/Helpers/RegistrationPackageDetails.cs index 6e4bd82f5..5a371c0cc 100644 --- a/tests/CatalogTests/Helpers/RegistrationPackageDetails.cs +++ b/tests/CatalogTests/Helpers/RegistrationPackageDetails.cs @@ -14,6 +14,8 @@ internal sealed class RegistrationPackageDetails internal string TypeKeyword { get; } [JsonProperty(CatalogConstants.Authors)] internal string Authors { get; } + [JsonProperty(CatalogConstants.Deprecation)] + internal RegistrationPackageDeprecation Deprecation { get; } [JsonProperty(CatalogConstants.Description)] internal string Description { get; } [JsonProperty(CatalogConstants.IconUrl)] @@ -50,6 +52,7 @@ internal RegistrationPackageDetails( string idKeyword, string typeKeyword, string authors, + RegistrationPackageDeprecation deprecation, string description, string iconUrl, string id, @@ -69,6 +72,7 @@ internal RegistrationPackageDetails( IdKeyword = idKeyword; TypeKeyword = typeKeyword; Authors = authors; + Deprecation = deprecation; Description = description; IconUrl = iconUrl; Id = id; diff --git a/tests/CatalogTests/Registration/RegistrationMakerCatalogItemTests.cs b/tests/CatalogTests/Registration/RegistrationMakerCatalogItemTests.cs index 742253a0b..27a961b7f 100644 --- a/tests/CatalogTests/Registration/RegistrationMakerCatalogItemTests.cs +++ b/tests/CatalogTests/Registration/RegistrationMakerCatalogItemTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; @@ -83,7 +84,7 @@ private static string GetContentString(StorageContent content) } } - public class CreatePageContent_SetsLicenseUrlAndExpression + public class TheCreatePageContentMethod { private readonly Uri _catalogUri = new Uri("http://example/catalog/mypackage.1.0.0.json"); private readonly Uri _registrationBaseAddress = new Uri("http://example/registration/"); @@ -91,7 +92,7 @@ public class CreatePageContent_SetsLicenseUrlAndExpression private readonly Uri _baseAddress = new Uri("http://example/registration/mypackage/"); private Graph _graph; - public CreatePageContent_SetsLicenseUrlAndExpression() + public TheCreatePageContentMethod() { _graph = new Graph(); _graph.Assert( @@ -122,8 +123,12 @@ public CreatePageContent_SetsLicenseUrlAndExpression() [InlineData(null, null, "/Folder/TestLicense", "http://gallery.org/packages/MyPackage/1.2.3/license", "http://gallery.org")] [InlineData(null, null, "TestLicense", "http://gallery.org/packages/MyPackage/1.2.3/license", "http://gallery.org/")] [InlineData(null, null, "TestLicense", "http://gallery.org/packages/MyPackage/1.2.3/license", "http://gallery.org//")] - public void CreatePageContent_SetsLicenseUrlAndExpressionProperly(string licenseUrl, string licenseExpression, string licenseFile, - string expectedLicenseUrlValue, string galleryBaseAddress) + public void CreatePageContent_SetsLicenseUrlAndExpressionProperly( + string licenseUrl, + string licenseExpression, + string licenseFile, + string expectedLicenseUrlValue, + string galleryBaseAddress) { // Arrange if (licenseUrl != null) @@ -180,6 +185,151 @@ public void CreatePageContent_SetsLicenseUrlAndExpressionProperly(string license Assert.Equal(licenseExpression == null ? "" : licenseExpression, licenseExpressionTriples.First().Object.ToString()); Assert.Equal(0, licenseFileTriples.Count()); } + + public static IEnumerable CreatePageContent_SetsDeprecationInformationProperly_Data + { + get + { + foreach (var reason in + new [] + { + new[] { "first" }, + new[] { "first", "second" } + }) + { + foreach (var message in new[] { null, "this is the message" }) + { + yield return new object[] { reason, message, null, null }; + yield return new object[] { reason, message, "theId", "homeOnTheRange" }; + } + } + } + } + + [Theory] + [MemberData(nameof(CreatePageContent_SetsDeprecationInformationProperly_Data))] + public void CreatePageContent_SetsDeprecationInformationProperly( + IEnumerable reasons, + string message, + string alternatePackageId, + string alternatePackageRange) + { + if (alternatePackageId == null && alternatePackageRange != null) + { + throw new ArgumentException("Must specify alternate package range if alternate package ID is specified."); + } + + if (alternatePackageId != null && alternatePackageRange == null) + { + throw new ArgumentException("Must specify alternate package ID if alternate package range is specified."); + } + + // Arrange + var rootNode = _graph.CreateUriNode(_catalogUri); + var deprecationPredicate = _graph.CreateUriNode(Schema.Predicates.Deprecation); + var deprecationRootNode = _graph.CreateUriNode(new Uri(_catalogUri.ToString() + "#deprecation")); + _graph.Assert(rootNode, deprecationPredicate, deprecationRootNode); + + var deprecationReasonRootNode = _graph.CreateUriNode(Schema.Predicates.Reasons); + foreach (var reason in reasons) + { + var reasonNode = _graph.CreateLiteralNode(reason); + _graph.Assert(deprecationRootNode, deprecationReasonRootNode, reasonNode); + } + + if (message != null) + { + _graph.Assert( + deprecationRootNode, + _graph.CreateUriNode(Schema.Predicates.Message), + _graph.CreateLiteralNode(message)); + } + + if (alternatePackageId != null) + { + var deprecationAlternatePackagePredicate = _graph.CreateUriNode(Schema.Predicates.AlternatePackage); + var deprecationAlternatePackageRootNode = _graph.CreateUriNode(new Uri(_catalogUri.ToString() + "#deprecation/alternatePackage")); + _graph.Assert(deprecationRootNode, deprecationAlternatePackagePredicate, deprecationAlternatePackageRootNode); + + _graph.Assert( + deprecationAlternatePackageRootNode, + _graph.CreateUriNode(Schema.Predicates.Id), + _graph.CreateLiteralNode(alternatePackageId)); + + _graph.Assert( + deprecationAlternatePackageRootNode, + _graph.CreateUriNode(Schema.Predicates.Range), + _graph.CreateLiteralNode(alternatePackageRange)); + } + + var item = new RegistrationMakerCatalogItem( + _catalogUri, + _graph, + _registrationBaseAddress, + isExistingItem: false, + packageContentBaseAddress: _packageContentBaseAddress) + { + BaseAddress = _baseAddress, + }; + RegistrationMakerCatalogItem.PackagePathProvider = new PackagesFolderPackagePathProvider(); + var context = new CatalogContext(); + + // Act + var content = item.CreatePageContent(context); + + // Assert + var deprecationObjectNode = _graph + .GetTriplesWithSubjectPredicate( + _graph.CreateUriNode(_catalogUri), + _graph.CreateUriNode(Schema.Predicates.Deprecation)) + .Single() + .Object; + + var deprecationTriples = _graph.GetTriplesWithSubject(deprecationObjectNode); + var reasonTriples = deprecationTriples + .Where(t => t.HasPredicate(_graph.CreateUriNode(Schema.Predicates.Reasons))); + + foreach (var reason in reasons) + { + Assert.Contains(reasonTriples, t => t.HasObject(_graph.CreateLiteralNode(reason))); + } + + if (message == null) + { + Assert.DoesNotContain( + deprecationTriples, + t => t.HasPredicate(_graph.CreateUriNode(Schema.Predicates.Message))); + } + else + { + Assert.Contains( + deprecationTriples, + t => t.HasPredicate(_graph.CreateUriNode(Schema.Predicates.Message)) && t.HasObject(_graph.CreateLiteralNode(message))); + } + + if (alternatePackageId == null) + { + Assert.DoesNotContain( + deprecationTriples, + t => t.HasPredicate(_graph.CreateUriNode(Schema.Predicates.AlternatePackage))); + } + else + { + var alternatePackageObjectNode = _graph + .GetTriplesWithSubjectPredicate( + deprecationObjectNode, + _graph.CreateUriNode(Schema.Predicates.AlternatePackage)) + .Single() + .Object; + + var alternatePackageTriples = _graph.GetTriplesWithSubject(alternatePackageObjectNode); + Assert.Contains(alternatePackageTriples, + t => t.HasPredicate(_graph.CreateUriNode(Schema.Predicates.Id)) && t.HasObject(_graph.CreateLiteralNode(alternatePackageId))); + + Assert.Contains(alternatePackageTriples, + t => t.HasPredicate(_graph.CreateUriNode(Schema.Predicates.Range)) && t.HasObject(_graph.CreateLiteralNode(alternatePackageRange))); + } + } } } } \ No newline at end of file diff --git a/tests/CatalogTests/Registration/RegistrationMakerTests.cs b/tests/CatalogTests/Registration/RegistrationMakerTests.cs index 6ac0cff7c..717afb599 100644 --- a/tests/CatalogTests/Registration/RegistrationMakerTests.cs +++ b/tests/CatalogTests/Registration/RegistrationMakerTests.cs @@ -167,6 +167,69 @@ public async Task ProcessAsync_WithCustomPackageCountThreshold_TransitionsToPage } } + [Fact] + public async Task ProcessAsync_WithDeprecationInformation_FormatsPageCorrectly() + { + const int partitionSize = 1; + var pages = new List(); + IReadOnlyDictionary newItem; + MemoryStorage storage; + IReadOnlyList expectedPages; + + var packageDetailsList = new[] + { + // No deprecation + new CatalogIndependentPackageDetails( + id: "deprecationTests", + version: "2.4.3"), + + // Single reason + new CatalogIndependentPackageDetails( + id: "deprecationTests", + version: "3.6.5", + deprecation: new RegistrationPackageDeprecation( + new[] {"r1" })), + + // Message + new CatalogIndependentPackageDetails( + id: "deprecationTests", + version: "4.7.6", + deprecation: new RegistrationPackageDeprecation( + new[] {"r1", "r2" }, + "the cow goes moo")), + + // Alternate package + new CatalogIndependentPackageDetails( + id: "deprecationTests", + version: "5.9.8", + deprecation: new RegistrationPackageDeprecation( + new[] {"r1", "r2" }, + null, + new RegistrationPackageDeprecationAlternatePackage("altPkg", "breezepackages"))), + + // Message and alternate package + new CatalogIndependentPackageDetails( + id: "deprecationTests", + version: "6.0.2", + deprecation: new RegistrationPackageDeprecation( + new[] {"r1", "r2" }, + "the package goes nuu", + new RegistrationPackageDeprecationAlternatePackage("altPkg", "breezepackages"))), + }; + + foreach (var packageDetails in packageDetailsList) + { + newItem = CreateNewItem(packageDetails); + storage = await ProcessAsync(newItem, packageDetails.Id, partitionSize); + + pages.Add(new ExpectedPage(packageDetails)); + + expectedPages = Repaginate(pages, partitionSize); + + Verify(storage, expectedPages, partitionSize); + } + } + private static IReadOnlyDictionary CreateNewItem(CatalogIndependentPackageDetails packageDetails) { var json = JsonConvert.SerializeObject(packageDetails, _jsonSettings); @@ -321,6 +384,9 @@ private void VerifyRegistrationIndex( new JObject( new JProperty(CatalogConstants.ContainerKeyword, CatalogConstants.SetKeyword), new JProperty(CatalogConstants.IdKeyword, CatalogConstants.Tag))), + new JProperty(CatalogConstants.Reasons, + new JObject( + new JProperty(CatalogConstants.ContainerKeyword, CatalogConstants.SetKeyword))), new JProperty(CatalogConstants.PackageTargetFrameworks, new JObject( new JProperty(CatalogConstants.ContainerKeyword, CatalogConstants.SetKeyword), @@ -446,6 +512,34 @@ private void VerifyRegistrationPackage( Assert.Empty(package.CatalogEntry.Title); Assert.Equal(packageDetails.Version, package.CatalogEntry.Version); + var actualDeprecation = package.CatalogEntry.Deprecation; + var expectedDeprecation = packageDetails.Deprecation; + if (expectedDeprecation == null) + { + Assert.Null(actualDeprecation); + } + else + { + Assert.NotNull(actualDeprecation); + + Assert.Equal(expectedDeprecation.Reasons.OrderBy(r => r), actualDeprecation.Reasons.OrderBy(r => r)); + Assert.Equal(expectedDeprecation.Message, actualDeprecation.Message); + + var actualDeprecationAltPackage = actualDeprecation.AlternatePackage; + var expectedDeprecationAltPackage = expectedDeprecation.AlternatePackage; + if (expectedDeprecationAltPackage == null) + { + Assert.Null(actualDeprecationAltPackage); + } + else + { + Assert.NotNull(actualDeprecationAltPackage); + + Assert.Equal(expectedDeprecationAltPackage.Id, actualDeprecationAltPackage.Id); + Assert.Equal(expectedDeprecationAltPackage.Range, actualDeprecationAltPackage.Range); + } + } + var independentPackageUri = GetRegistrationPackageVersionUri(packageId, packageVersion); var independentPackage = GetStorageContent( registrationStorage, @@ -527,6 +621,9 @@ private static JObject GetExpectedIndexOrPageContext() new JObject( new JProperty(CatalogConstants.ContainerKeyword, CatalogConstants.SetKeyword), new JProperty(CatalogConstants.IdKeyword, CatalogConstants.Tag))), + new JProperty(CatalogConstants.Reasons, + new JObject( + new JProperty(CatalogConstants.ContainerKeyword, CatalogConstants.SetKeyword))), new JProperty(CatalogConstants.PackageTargetFrameworks, new JObject( new JProperty(CatalogConstants.ContainerKeyword, CatalogConstants.SetKeyword), diff --git a/tests/NgTests/CatalogConstants.cs b/tests/NgTests/CatalogConstants.cs index d3008e952..9b6bb8305 100644 --- a/tests/NgTests/CatalogConstants.cs +++ b/tests/NgTests/CatalogConstants.cs @@ -5,6 +5,7 @@ namespace NgTests { public static class CatalogConstants { + public const string AlternatePackage = "alternatePackage"; public const string AppendOnlyCatalog = "AppendOnlyCatalog"; public const string Authors = "authors"; public const string Catalog = "catalog"; @@ -31,6 +32,7 @@ public static class CatalogConstants public const string Dependency = "dependency"; public const string DependencyGroup = "dependencyGroup"; public const string DependencyGroups = "dependencyGroups"; + public const string Deprecation = "deprecation"; public const string Description = "description"; public const string Details = "details"; public const string Entries = "entries"; @@ -48,6 +50,7 @@ public static class CatalogConstants public const string Links = "links"; public const string Listed = "listed"; public const string Lower = "lower"; + public const string Message = "message"; public const string MinClientVersion = "minClientVersion"; public const string Name = "name"; public const string NuGet = "nuget"; @@ -78,6 +81,8 @@ public static class CatalogConstants public const string Permalink = "Permalink"; public const string ProjectUrl = "projectUrl"; public const string Published = "published"; + public const string Range = "range"; + public const string Reasons = "reasons"; public const string RequireLicenseAcceptance = "requireLicenseAcceptance"; public const string Registration = "registration"; public const string SetKeyword = "@set";