diff --git a/src/BaGetter.Core/Extensions/PackageArchiveReaderExtensions.cs b/src/BaGetter.Core/Extensions/PackageArchiveReaderExtensions.cs index d87d0d38f..129100350 100644 --- a/src/BaGetter.Core/Extensions/PackageArchiveReaderExtensions.cs +++ b/src/BaGetter.Core/Extensions/PackageArchiveReaderExtensions.cs @@ -19,7 +19,7 @@ public static bool HasReadme(this PackageArchiveReader package) public static bool HasEmbeddedIcon(this PackageArchiveReader package) => !string.IsNullOrEmpty(package.NuspecReader.GetIcon()); - public async static Task GetReadmeAsync( + public static async Task GetReadmeAsync( this PackageArchiveReader package, CancellationToken cancellationToken) { @@ -34,7 +34,7 @@ public async static Task GetReadmeAsync( return await package.GetStreamAsync(readmePath, cancellationToken); } - public async static Task GetIconAsync( + public static async Task GetIconAsync( this PackageArchiveReader package, CancellationToken cancellationToken) { @@ -111,18 +111,42 @@ private static Uri ParseUri(string uriString) private static readonly char[] Separator = { ',', ';', '\t', '\n', '\r' }; + /// + /// Parses the authors into a list of authors. + /// + /// + /// Authors are delimited by comma.
+ /// See: + ///
+ /// authors to be parsed + /// A list of authors. private static string[] ParseAuthors(string authors) { - if (string.IsNullOrEmpty(authors)) return Array.Empty(); + if (string.IsNullOrEmpty(authors)) + { + return Array.Empty(); + } return authors.Split(Separator, StringSplitOptions.RemoveEmptyEntries); } + /// + /// Parses the tags into a list of tags. + /// + /// + /// Tags are delimited by space.
+ /// See: + ///
+ /// tags to be parsed + /// A list of tags. private static string[] ParseTags(string tags) { - if (string.IsNullOrEmpty(tags)) return Array.Empty(); + if (string.IsNullOrEmpty(tags)) + { + return Array.Empty(); + } - return tags.Split(Separator, StringSplitOptions.RemoveEmptyEntries); + return tags.Split(' ', StringSplitOptions.RemoveEmptyEntries); } private static (Uri repositoryUrl, string repositoryType) GetRepositoryMetadata(NuspecReader nuspec) diff --git a/src/BaGetter.Core/Upstream/Clients/V3UpstreamClient.cs b/src/BaGetter.Core/Upstream/Clients/V3UpstreamClient.cs index f0e45bd52..eb91621a1 100644 --- a/src/BaGetter.Core/Upstream/Clients/V3UpstreamClient.cs +++ b/src/BaGetter.Core/Upstream/Clients/V3UpstreamClient.cs @@ -6,8 +6,8 @@ using System.Threading.Tasks; using BaGetter.Protocol; using BaGetter.Protocol.Models; -using NuGet.Versioning; using Microsoft.Extensions.Logging; +using NuGet.Versioning; namespace BaGetter.Core; @@ -18,7 +18,8 @@ public class V3UpstreamClient : IUpstreamClient { private readonly NuGetClient _client; private readonly ILogger _logger; - private static readonly char[] Separator = { ',', ';', '\t', '\n', '\r' }; + private static readonly char[] AuthorSeparator = [',', ';', '\t', '\n', '\r']; + private static readonly char[] TagSeparator = [' ']; public V3UpstreamClient(NuGetClient client, ILogger logger) { @@ -110,7 +111,7 @@ private Package ToPackage(PackageMetadata metadata) RepositoryUrl = null, RepositoryType = null, SemVerLevel = version.IsSemVer2 ? SemVerLevel.SemVer2 : SemVerLevel.Unknown, - Tags = metadata.Tags?.ToArray() ?? Array.Empty(), + Tags = ParseTags(metadata.Tags), Dependencies = ToDependencies(metadata) }; @@ -130,11 +131,23 @@ private static Uri ParseUri(string uriString) private static string[] ParseAuthors(string authors) { - if (string.IsNullOrEmpty(authors)) return Array.Empty(); + if (string.IsNullOrEmpty(authors)) + { + return Array.Empty(); + } + + return authors.Split(AuthorSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + private static string[] ParseTags(IEnumerable tags) + { + if (tags is null) + { + return Array.Empty(); + } - return authors - .Split(Separator, StringSplitOptions.RemoveEmptyEntries) - .Select(a => a.Trim()) + return tags + .SelectMany(t => t.Split(TagSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) .ToArray(); } diff --git a/src/BaGetter.Protocol/Models/PackageMetadata.cs b/src/BaGetter.Protocol/Models/PackageMetadata.cs index 810a27150..ddd1bca8e 100644 --- a/src/BaGetter.Protocol/Models/PackageMetadata.cs +++ b/src/BaGetter.Protocol/Models/PackageMetadata.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; +using BaGetter.Protocol.Internal; namespace BaGetter.Protocol.Models; @@ -117,6 +118,7 @@ public class PackageMetadata /// The package's tags. /// [JsonPropertyName("tags")] + [JsonConverter(typeof(StringOrStringArrayJsonConverter))] public IReadOnlyList Tags { get; set; } /// diff --git a/tests/BaGetter.Core.Tests/Upstream/V3UpstreamClientTests.cs b/tests/BaGetter.Core.Tests/Upstream/V3UpstreamClientTests.cs index 78f87e484..4090af2f6 100644 --- a/tests/BaGetter.Core.Tests/Upstream/V3UpstreamClientTests.cs +++ b/tests/BaGetter.Core.Tests/Upstream/V3UpstreamClientTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; @@ -115,7 +115,7 @@ public async Task ReturnsPackages() Summary = "Summary", Title = "Title", - Tags = new List { "Tag1", "Tag2" }, + Tags = new List { "Tag1 Tag2" }, Deprecation = new PackageDeprecation { @@ -150,7 +150,7 @@ public async Task ReturnsPackages() var package = Assert.Single(result); Assert.Equal("Foo", package.Id); - Assert.Equal(new[] { "Author1", "Author2"}, package.Authors); + Assert.Equal(new[] { "Author1", "Author2" }, package.Authors); Assert.Equal("Description", package.Description); Assert.False(package.HasReadme); Assert.False(package.HasEmbeddedIcon); diff --git a/tests/BaGetter.Protocol.Tests/Models/PackageMetadataTests.cs b/tests/BaGetter.Protocol.Tests/Models/PackageMetadataTests.cs new file mode 100644 index 000000000..e7453cb50 --- /dev/null +++ b/tests/BaGetter.Protocol.Tests/Models/PackageMetadataTests.cs @@ -0,0 +1,150 @@ +using System.Text.Json; +using BaGetter.Protocol.Internal; +using BaGetter.Protocol.Models; +using Xunit; + +namespace BaGetter.Protocol.Tests.Models; + +public class PackageMetadataTests +{ + private readonly JsonSerializerOptions _serializerOptions; + + public PackageMetadataTests() + { + _serializerOptions = new JsonSerializerOptions(); + _serializerOptions.Converters.Add(new StringOrStringArrayJsonConverter()); + } + + [Fact] + public void Tags_EmptyArray_ShouldDeserialize() + { + // Arrange + var stringToDeserialize = """ + { + "@id": "https://nuget.pkg.github.com/FakeOrga/fakepkg/1.5.1.json", + "authors": "Someone", + "copyright": "", + "dependencyGroups": [ ], + "description": "Package Description", + "iconUrl": "", + "id": "fakepkg", + "isPrerelease": false, + "language": "", + "licenseUrl": "", + "packageContent": "https://nuget.pkg.github.com/FakeOrga/download/fakepkg/1.5.1/fakepkg.1.5.1.nupkg", + "projectUrl": "", + "requireLicenseAcceptance": false, + "summary": "", + "tags": [ ], + "version": "1.5.1" + } + """; + + // Act + var result = JsonSerializer.Deserialize(stringToDeserialize, _serializerOptions); + + // Assert + Assert.NotNull(result.Tags); + Assert.Empty(result.Tags); + } + + [Fact] + public void Tags_EmptyString_ShouldDeserialize() + { + // Arrange + var stringToDeserialize = """ + { + "@id": "https://nuget.pkg.github.com/FakeOrga/fakepkg/1.5.1.json", + "authors": "Someone", + "copyright": "", + "dependencyGroups": [ ], + "description": "Package Description", + "iconUrl": "", + "id": "fakepkg", + "isPrerelease": false, + "language": "", + "licenseUrl": "", + "packageContent": "https://nuget.pkg.github.com/FakeOrga/download/fakepkg/1.5.1/fakepkg.1.5.1.nupkg", + "projectUrl": "", + "requireLicenseAcceptance": false, + "summary": "", + "tags": "", + "version": "1.5.1" + } + """; + + // Act + var result = JsonSerializer.Deserialize(stringToDeserialize, _serializerOptions); + + // Assert + Assert.NotNull(result.Tags); + Assert.Single(result.Tags); + } + + [Fact] + public void Tags_SingleEntry_ShouldDeserializeSingleTag() + { + // Arrange + var stringToDeserialize = """ + { + "@id": "https://nuget.pkg.github.com/FakeOrga/fakepkg/1.5.1.json", + "authors": "Someone", + "copyright": "", + "dependencyGroups": [ ], + "description": "Package Description", + "iconUrl": "", + "id": "fakepkg", + "isPrerelease": false, + "language": "", + "licenseUrl": "", + "packageContent": "https://nuget.pkg.github.com/FakeOrga/download/fakepkg/1.5.1/fakepkg.1.5.1.nupkg", + "projectUrl": "", + "requireLicenseAcceptance": false, + "summary": "", + "tags": "tag1", + "version": "1.5.1" + } + """; + + // Act + var result = JsonSerializer.Deserialize(stringToDeserialize, _serializerOptions); + + // Assert + Assert.NotNull(result.Tags); + Assert.Single(result.Tags); + } + + [Fact] + public void Tags_HasMultipleEntries_ShouldDeserializeMultipleTags() + { + // Arrange + var stringToDeserialize = """ + { + "@id": "https://nuget.pkg.github.com/FakeOrga/fakepkg/1.5.1.json", + "authors": "Someone", + "copyright": "", + "dependencyGroups": [ ], + "description": "Package Description", + "iconUrl": "", + "id": "fakepkg", + "isPrerelease": false, + "language": "", + "licenseUrl": "", + "packageContent": "https://nuget.pkg.github.com/FakeOrga/download/fakepkg/1.5.1/fakepkg.1.5.1.nupkg", + "projectUrl": "", + "requireLicenseAcceptance": false, + "summary": "", + "tags": ["tag1", "tag2"], + "version": "1.5.1" + } + """; + + // Act + var result = JsonSerializer.Deserialize(stringToDeserialize, _serializerOptions); + + // Assert + Assert.NotNull(result.Tags); + Assert.Equal(2, result.Tags.Count); + } + +} diff --git a/tests/BaGetter.Protocol.Tests/RawPackageMetadataClientTests.cs b/tests/BaGetter.Protocol.Tests/RawPackageMetadataClientTests.cs index 9dbf6cba4..ec91d34c7 100644 --- a/tests/BaGetter.Protocol.Tests/RawPackageMetadataClientTests.cs +++ b/tests/BaGetter.Protocol.Tests/RawPackageMetadataClientTests.cs @@ -1,6 +1,5 @@ using System.Threading.Tasks; using BaGetter.Protocol.Internal; -using NuGet.Versioning; using Xunit; namespace BaGetter.Protocol.Tests; @@ -35,6 +34,35 @@ public async Task GetRegistrationIndexInlinedItems() Assert.StartsWith(TestData.RegistrationIndexInlinedItemsUrl, result.Pages[1].RegistrationPageUrl); } + [Fact] + public async Task GetRegistrationIndexLikeGithubPackages() + { + var result = await _target.GetRegistrationIndexOrNullAsync("my.github.pkg"); + + Assert.NotNull(result); + Assert.Equal(1, result.Count); + Assert.Collection(result.Pages, page => + { + Assert.Equal(2, page.Count); + Assert.Collection(page.ItemsOrNull, + pkg1 => + { + Assert.Equal("2.1.6", pkg1.PackageMetadata.Version); + Assert.Collection(pkg1.PackageMetadata.Tags, + tag1 => Assert.Equal("json bson serializer", tag1) + ); + }, + pkg2 => + { + Assert.Equal("2.1.5", pkg2.PackageMetadata.Version); + Assert.Collection(pkg2.PackageMetadata.Tags, + tag1 => Assert.Equal("", tag1) + ); + } + ); + }); + } + [Fact] public async Task GetRegistrationIndexPagedItems() { diff --git a/tests/BaGetter.Protocol.Tests/Support/TestDataHttpMessageHandler.cs b/tests/BaGetter.Protocol.Tests/Support/TestDataHttpMessageHandler.cs index 2621154a7..d9ee35017 100644 --- a/tests/BaGetter.Protocol.Tests/Support/TestDataHttpMessageHandler.cs +++ b/tests/BaGetter.Protocol.Tests/Support/TestDataHttpMessageHandler.cs @@ -21,6 +21,7 @@ public class TestDataHttpMessageHandler : HttpMessageHandler //{ TestData.CatalogLeafInvalidDependencyVersionRangeUrl, () => TestData.CatalogLeafInvalidDependencyVersionRange }, { TestData.RegistrationIndexInlinedItemsUrl, () => TestData.RegistrationIndexInlinedItems }, + { TestData.RegistrationIndexLikeGithubPackagesUrl, () => TestData.RegistrationIndexLikeGithubPackages }, { TestData.RegistrationIndexPagedItemsUrl, () => TestData.RegistrationIndexPagedItems }, { TestData.RegistrationLeafUnlistedUrl, () => TestData.RegistrationLeafUnlisted }, { TestData.RegistrationLeafListedUrl, () => TestData.RegistrationLeafListed }, diff --git a/tests/BaGetter.Protocol.Tests/TestData.Designer.cs b/tests/BaGetter.Protocol.Tests/TestData.Designer.cs index 5cb9d60cb..fb7e963de 100644 --- a/tests/BaGetter.Protocol.Tests/TestData.Designer.cs +++ b/tests/BaGetter.Protocol.Tests/TestData.Designer.cs @@ -19,7 +19,7 @@ namespace BaGetter.Protocol.Tests { // 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", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class TestData { @@ -348,6 +348,36 @@ internal static string RegistrationIndexInlinedItemsUrl { } } + /// + /// Looks up a localized string similar to { + /// "count": 1, + /// "items": [ + /// { + /// "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/index.json", + /// "lower": "2.1.5", + /// "upper": "2.1.6", + /// "count": 2, + /// "items": [ + /// { + /// "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/2.1.6.json", + /// "packageContent": "https://nuget.pkg.github.com/metadata/download/my.github.pkg/2.1.6/my.github.pkg.2.1.6.nupkg", + /// "catalogEntr [rest of string was truncated]";. + /// + internal static string RegistrationIndexLikeGithubPackages { + get { + return ResourceManager.GetString("RegistrationIndexLikeGithubPackages", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to https://test.example/v3/metadata/my.github.pkg/index.json. + /// + internal static string RegistrationIndexLikeGithubPackagesUrl { + get { + return ResourceManager.GetString("RegistrationIndexLikeGithubPackagesUrl", resourceCulture); + } + } + /// /// Looks up a localized string similar to { /// "@id": "https://test.example/v3/metadata/paged.package/index.json", diff --git a/tests/BaGetter.Protocol.Tests/TestData.resx b/tests/BaGetter.Protocol.Tests/TestData.resx index bcc332ccd..2fb8b1e50 100644 --- a/tests/BaGetter.Protocol.Tests/TestData.resx +++ b/tests/BaGetter.Protocol.Tests/TestData.resx @@ -723,6 +723,70 @@ https://test.example/v3/metadata/test.package/index.json + + { + "count": 1, + "items": [ + { + "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/index.json", + "lower": "2.1.5", + "upper": "2.1.6", + "count": 2, + "items": [ + { + "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/2.1.6.json", + "packageContent": "https://nuget.pkg.github.com/metadata/download/my.github.pkg/2.1.6/my.github.pkg.2.1.6.nupkg", + "catalogEntry": { + "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/2.1.6.json", + "authors": "Someone", + "copyright": "", + "dependencyGroups": [ + ], + "description": "Nice library", + "iconUrl": "", + "id": "my.github.pkg", + "isPrerelease": false, + "language": "", + "licenseUrl": "https://licenses.nuget.org/MIT", + "packageContent": "https://nuget.pkg.github.com/metadata/download/my.github.pkg/2.1.6/my.github.pkg.2.1.6.nupkg", + "projectUrl": "https://github.com/User/Project", + "requireLicenseAcceptance": false, + "summary": "", + "tags": "json bson serializer", + "version": "2.1.6" + } + }, + { + "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/2.1.5.json", + "packageContent": "https://nuget.pkg.github.com/metadata/download/my.github.pkg/2.1.5/my.github.pkg.2.1.5.nupkg", + "catalogEntry": { + "@id": "https://nuget.pkg.github.com/metadata/my.github.pkg/2.1.5.json", + "authors": "Someone", + "copyright": "", + "dependencyGroups": [ + ], + "description": "Nice library", + "iconUrl": "", + "id": "my.github.pkg", + "isPrerelease": false, + "language": "", + "licenseUrl": "https://licenses.nuget.org/MIT", + "packageContent": "https://nuget.pkg.github.com/metadata/download/my.github.pkg/2.1.5/my.github.pkg.2.1.5.nupkg", + "projectUrl": "https://github.com/User/Project", + "requireLicenseAcceptance": false, + "summary": "", + "tags": "", + "version": "2.1.5" + } + } + ] + } + ] +} + + + https://test.example/v3/metadata/my.github.pkg/index.json + { "@id": "https://test.example/v3/metadata/paged.package/index.json",