From 821c093b1c2414fa31ba4302b1e92270d3668328 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 26 Sep 2021 17:27:35 -0700 Subject: [PATCH 1/7] Start Push more logic to mirror service Refactor search Refactor Azure Table search Clean --- src/BaGet.Core/IUrlGenerator.cs | 20 +- .../Metadata/RegistrationBuilder.cs | 6 +- src/BaGet.Core/V2/IV2Builder.cs | 12 + src/BaGet.Core/V2/V2Builder.cs | 219 ++++++++++++++++++ src/BaGet.Web/BaGetEndpointBuilder.cs | 31 ++- src/BaGet.Web/BaGetUrlGenerator.cs | 45 +++- src/BaGet.Web/Controllers/V2ApiController.cs | 163 +++++++++++++ .../IServiceCollectionExtensions.cs | 1 + src/BaGet.Web/Pages/Package.cshtml.cs | 2 +- src/BaGet.Web/Pages/Upload.cshtml | 2 +- src/BaGet.Web/Routes.cs | 10 +- 11 files changed, 495 insertions(+), 16 deletions(-) create mode 100644 src/BaGet.Core/V2/IV2Builder.cs create mode 100644 src/BaGet.Core/V2/V2Builder.cs create mode 100644 src/BaGet.Web/Controllers/V2ApiController.cs diff --git a/src/BaGet.Core/IUrlGenerator.cs b/src/BaGet.Core/IUrlGenerator.cs index 443dba796..905c6917e 100644 --- a/src/BaGet.Core/IUrlGenerator.cs +++ b/src/BaGet.Core/IUrlGenerator.cs @@ -11,7 +11,7 @@ public interface IUrlGenerator /// Get the URL for the package source (also known as the "service index"). /// See: https://docs.microsoft.com/en-us/nuget/api/service-index /// - string GetServiceIndexUrl(); + string GetServiceIndexV3Url(); /// /// Get the URL for the root of the package content resource. @@ -80,6 +80,13 @@ public interface IUrlGenerator /// The package's ID string GetPackageVersionsUrl(string id); + /// + /// Get the URL to download a package (.nupkg). + /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg + /// + /// The package to download + string GetPackageDownloadUrl(Package package); + /// /// Get the URL to download a package (.nupkg). /// See: https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg @@ -101,5 +108,16 @@ public interface IUrlGenerator /// The package's ID /// The package's version string GetPackageIconDownloadUrl(string id, NuGetVersion version); + + /// + /// Get the URL for the package source that implements the legacy NuGet V2 API. + /// + string GetServiceIndexV2Url(); + + /// + /// Get the URL for the metadata of a single package version. + /// + /// The package to lookup + string GetPackageVersionV2Url(Package package); } } diff --git a/src/BaGet.Core/Metadata/RegistrationBuilder.cs b/src/BaGet.Core/Metadata/RegistrationBuilder.cs index 4090e9323..0916ad764 100644 --- a/src/BaGet.Core/Metadata/RegistrationBuilder.cs +++ b/src/BaGet.Core/Metadata/RegistrationBuilder.cs @@ -52,7 +52,7 @@ public virtual RegistrationLeafResponse BuildLeaf(Package package) Listed = package.Listed, Published = package.Published, RegistrationLeafUrl = _url.GetRegistrationLeafUrl(id, version), - PackageContentUrl = _url.GetPackageDownloadUrl(id, version), + PackageContentUrl = _url.GetPackageDownloadUrl(package), RegistrationIndexUrl = _url.GetRegistrationIndexUrl(id) }; } @@ -61,7 +61,7 @@ private BaGetRegistrationIndexPageItem ToRegistrationIndexPageItem(Package packa new BaGetRegistrationIndexPageItem { RegistrationLeafUrl = _url.GetRegistrationLeafUrl(package.Id, package.Version), - PackageContentUrl = _url.GetPackageDownloadUrl(package.Id, package.Version), + PackageContentUrl = _url.GetPackageDownloadUrl(package), PackageMetadata = new BaGetPackageMetadata { PackageId = package.Id, @@ -78,7 +78,7 @@ private BaGetRegistrationIndexPageItem ToRegistrationIndexPageItem(Package packa Listed = package.Listed, MinClientVersion = package.MinClientVersion, ReleaseNotes = package.ReleaseNotes, - PackageContentUrl = _url.GetPackageDownloadUrl(package.Id, package.Version), + PackageContentUrl = _url.GetPackageDownloadUrl(package), PackageTypes = package.PackageTypes.Select(t => t.Name).ToList(), ProjectUrl = package.ProjectUrlString, RepositoryUrl = package.RepositoryUrlString, diff --git a/src/BaGet.Core/V2/IV2Builder.cs b/src/BaGet.Core/V2/IV2Builder.cs new file mode 100644 index 000000000..2689b6706 --- /dev/null +++ b/src/BaGet.Core/V2/IV2Builder.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using System.Xml.Linq; + +namespace BaGet.Core +{ + public interface IV2Builder + { + XElement BuildIndex(); + XElement BuildPackages(IReadOnlyList packages); + XElement BuildPackage(Package package); + } +} diff --git a/src/BaGet.Core/V2/V2Builder.cs b/src/BaGet.Core/V2/V2Builder.cs new file mode 100644 index 000000000..245036175 --- /dev/null +++ b/src/BaGet.Core/V2/V2Builder.cs @@ -0,0 +1,219 @@ +using System.Collections.Generic; +using System.Linq; +using System.Xml.Linq; + +namespace BaGet.Core +{ + public class V2Builder : IV2Builder + { + private readonly IUrlGenerator _url; + + public V2Builder(IUrlGenerator url) + { + _url = url; + } + + public XElement BuildIndex() + { + var serviceIndex = _url.GetServiceIndexV2Url(); + + return XElement.Parse($@" + + + Default + + Packages + + +"); + } + + public XElement BuildPackages(IReadOnlyList packages) + { + // See: https://joelverhagen.github.io/NuGetUndocs/#endpoint-find-packages-by-id + var serviceIndex = _url.GetServiceIndexV2Url(); + + // TODO: Add to top + return new XElement( + N.feed, + new XAttribute(N.baze, XNamespace.Get(serviceIndex)), + new XAttribute(N.m, NS.m), + new XAttribute(N.d, NS.d), + new XAttribute(N.georss, NS.georss), + new XAttribute(N.gml, NS.gml), + new XElement(N.m_count, packages.Count), + + packages.Select(package => + { + var packageV2Url = _url.GetPackageVersionV2Url(package); + var downloadUrl = _url.GetPackageDownloadUrl(package); + + return new XElement( + N.entry, + new XElement(N.id, packageV2Url), + new XElement(N.title, package.Title), + new XElement( + N.content, + new XAttribute("type", "application/zip"), + new XAttribute("src", downloadUrl) + ), + + BuildAuthor(package), + BuildProperties(package) + ); + }) + ); + } + + public XElement BuildPackage(Package package) + { + // See: https://joelverhagen.github.io/NuGetUndocs/#endpoint-get-a-single-package + var serviceIndex = _url.GetServiceIndexV2Url(); + var packageV2Url = _url.GetPackageVersionV2Url(package); + var downloadUrl = _url.GetPackageDownloadUrl(package); + + return new XElement( + N.entry, + new XAttribute(N.baze, XNamespace.Get(serviceIndex)), + new XAttribute(N.m, NS.m), + new XAttribute(N.d, NS.d), + new XAttribute(N.georss, NS.georss), + new XAttribute(N.gml, NS.gml), + new XElement(N.id, packageV2Url), + new XElement(N.title, package.Title), + + new XElement( + N.content, + new XAttribute("type", "application/zip"), + new XAttribute("src", downloadUrl) + ), + + BuildAuthor(package), + BuildProperties(package) + ); + } + + private XElement BuildProperties(Package package) + { + // See: https://joelverhagen.github.io/NuGetUndocs/#package-entity + return new XElement( + N.m_properties, + new XElement(N.d_Id, package.Id), + new XElement(N.d_Title, package.Title), + new XElement(N.d_Version, package.OriginalVersionString), + new XElement(N.d_NormalizedVersion, package.NormalizedVersionString), + new XElement(N.d_Authors, string.Join(", ", package.Authors)), + new XElement(N.d_Copyright, ""), // TODO + new XElement(N.d_Description, package.Description), + new XElement( + N.d_DownloadCount, + new XAttribute(N.m_type, "Edm.Int32"), + package.Downloads), + new XElement(N.d_LastEdited, package.Published), + new XElement(N.d_Published, package.Published), + new XElement(N.d_PackageHash, ""), + new XElement(N.d_PackageHashAlgorithm, ""), + new XElement(N.d_PackageSize, 0), + new XElement(N.d_ProjectUrl, package.ProjectUrl), + new XElement(N.d_IconUrl, package.IconUrl), // TODO, URL logic + new XElement(N.d_LicenseUrl, package.LicenseUrl), // TODO + new XElement(N.d_Tags, string.Join(", ", package.Tags)), + new XElement(N.d_RequireLicenseAcceptance, package.RequireLicenseAcceptance), + + BuildDependencies(package) + ); + } + + private XElement BuildAuthor(Package package) + { + // TODO: No authors? + return new XElement( + N.author, + package.Authors.Select(author => new XElement(N.name, author)) + ); + } + + private XElement BuildDependencies(Package package) + { + var flattenedDependencies = new List(); + + flattenedDependencies.AddRange( + package + .Dependencies + .Where(IsFrameworkDependency) + .Select(dependency => dependency.TargetFramework) + .Distinct() + .Select(targetFramework => $"::{targetFramework}")); + + flattenedDependencies.AddRange( + package + .Dependencies + .Where(dependency => !IsFrameworkDependency(dependency)) + .Select(dependency => $"{dependency.Id}:{dependency.VersionRange}:{dependency.TargetFramework}")); + + var result = string.Join("|", flattenedDependencies); + + return new XElement(N.d_Dependencies, result); + } + + private bool IsFrameworkDependency(PackageDependency dependency) + { + return dependency.Id == null && dependency.VersionRange == null; + } + + private static class NS + { + public static readonly XNamespace xmlns = "http://www.w3.org/2005/Atom"; + //public static readonly XNamespace baze = "https://www.nuget.org/api/v2/curated-feeds/microsoftdotnet"; + public static readonly XNamespace m = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; + public static readonly XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices"; + public static readonly XNamespace georss = "http://www.georss.org/georss"; + public static readonly XNamespace gml = "http://www.opengis.net/gml"; + } + + private static class N + { + public static readonly XName feed = NS.xmlns + "feed"; + public static readonly XName entry = NS.xmlns + "entry"; + public static readonly XName title = NS.xmlns + "title"; + public static readonly XName author = NS.xmlns + "author"; + public static readonly XName name = NS.xmlns + "name"; + public static readonly XName link = NS.xmlns + "link"; + public static readonly XName id = NS.xmlns + "id"; + public static readonly XName content = NS.xmlns + "content"; + + public static readonly XName m_count = NS.m + "count"; + public static readonly XName m_properties = NS.m + "properties"; + public static readonly XName m_type = NS.m + "type"; + + public static readonly XName d_Id = NS.d + "Id"; + public static readonly XName d_Title = NS.d + "Title"; + public static readonly XName d_Version = NS.d + "Version"; + public static readonly XName d_NormalizedVersion = NS.d + "NormalizedVersion"; + public static readonly XName d_Authors = NS.d + "Authors"; + public static readonly XName d_Copyright = NS.d + "Copyright"; + public static readonly XName d_Dependencies = NS.d + "Dependencies"; + public static readonly XName d_Description = NS.d + "Description"; + public static readonly XName d_IconUrl = NS.d + "IconUrl"; + public static readonly XName d_LicenseUrl = NS.d + "LicenseUrl"; + public static readonly XName d_ProjectUrl = NS.d + "ProjectUrl"; + public static readonly XName d_Tags = NS.d + "Tags"; + public static readonly XName d_ReportAbuseUrl = NS.d + "ReportAbuseUrl"; + public static readonly XName d_RequireLicenseAcceptance = NS.d + "RequireLicenseAcceptance"; + public static readonly XName d_DownloadCount = NS.d + "DownloadCount"; + public static readonly XName d_Created = NS.d + "Created"; + public static readonly XName d_LastEdited = NS.d + "LastEdited"; + public static readonly XName d_Published = NS.d + "Published"; + public static readonly XName d_PackageHash = NS.d + "PackageHash"; + public static readonly XName d_PackageHashAlgorithm = NS.d + "PackageHashAlgorithm"; + public static readonly XName d_MinClientVersion = NS.d + "MinClientVersion"; + public static readonly XName d_PackageSize = NS.d + "PackageSize"; + + public static readonly XName baze = XNamespace.Xmlns + "base"; + public static readonly XName m = XNamespace.Xmlns + "m"; + public static readonly XName d = XNamespace.Xmlns + "d"; + public static readonly XName georss = XNamespace.Xmlns + "georss"; + public static readonly XName gml = XNamespace.Xmlns + "gml"; + } + } +} diff --git a/src/BaGet.Web/BaGetEndpointBuilder.cs b/src/BaGet.Web/BaGetEndpointBuilder.cs index d485eb40e..642d02d06 100644 --- a/src/BaGet.Web/BaGetEndpointBuilder.cs +++ b/src/BaGet.Web/BaGetEndpointBuilder.cs @@ -17,12 +17,13 @@ public void MapEndpoints(IEndpointRouteBuilder endpoints) MapSearchRoutes(endpoints); MapPackageMetadataRoutes(endpoints); MapPackageContentRoutes(endpoints); + MapV2ApiRoutes(endpoints); } public void MapServiceIndexRoutes(IEndpointRouteBuilder endpoints) { endpoints.MapControllerRoute( - name: Routes.IndexRouteName, + name: Routes.V3IndexRouteName, pattern: "v3/index.json", defaults: new { controller = "ServiceIndex", action = "Get" }); } @@ -126,5 +127,33 @@ public void MapPackageContentRoutes(IEndpointRouteBuilder endpoints) pattern: "v3/package/{id}/{version}/icon", defaults: new { controller = "PackageContent", action = "DownloadIcon" }); } + + public void MapV2ApiRoutes(IEndpointRouteBuilder endpoints) + { + endpoints.MapControllerRoute( + name: Routes.V2IndexRouteName, + pattern: "api/v2", + defaults: new { controller = "V2Api", action = "Index" }); + + endpoints.MapControllerRoute( + name: Routes.V2ListRouteName, + pattern: "api/v2/Packages()", + defaults: new { controller = "V2Api", action = "List" }); + + endpoints.MapControllerRoute( + name: Routes.V2SearchRouteName, + pattern: "api/v2/Search()", + defaults: new { controller = "V2Api", action = "Search" }); + + endpoints.MapControllerRoute( + name: Routes.V2PackageRouteName, + pattern: "api/v2/FindPackagesById()", + defaults: new { controller = "V2Api", action = "Package" }); + + endpoints.MapControllerRoute( + name: Routes.V2PackageVersionRouteName, + pattern: "api/v2/Packages(Id='{id}',Version='{version}')", + defaults: new { controller = "V2Api", action = "PackageVersion" }); + } } } diff --git a/src/BaGet.Web/BaGetUrlGenerator.cs b/src/BaGet.Web/BaGetUrlGenerator.cs index 48b61bb57..66d2dae63 100644 --- a/src/BaGet.Web/BaGetUrlGenerator.cs +++ b/src/BaGet.Web/BaGetUrlGenerator.cs @@ -18,11 +18,11 @@ public BaGetUrlGenerator(IHttpContextAccessor httpContextAccessor, LinkGenerator _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); } - public string GetServiceIndexUrl() + public string GetServiceIndexV3Url() { return _linkGenerator.GetUriByRouteValues( _httpContextAccessor.HttpContext, - Routes.IndexRouteName, + Routes.V3IndexRouteName, values: null); } @@ -102,19 +102,30 @@ public string GetPackageVersionsUrl(string id) values: new { Id = id.ToLowerInvariant() }); } + public string GetPackageDownloadUrl(Package package) + { + return GetPackageDownloadUrl( + package.Id.ToLowerInvariant(), + package.NormalizedVersionString.ToLowerInvariant()); + } + public string GetPackageDownloadUrl(string id, NuGetVersion version) { - id = id.ToLowerInvariant(); - var versionString = version.ToNormalizedString().ToLowerInvariant(); + return GetPackageDownloadUrl( + id.ToLowerInvariant(), + version.ToNormalizedString().ToLowerInvariant()); + } + private string GetPackageDownloadUrl(string lowerId, string lowerVersion) + { return _linkGenerator.GetUriByRouteValues( _httpContextAccessor.HttpContext, Routes.PackageDownloadRouteName, values: new { - Id = id, - Version = versionString, - IdVersion = $"{id}.{versionString}" + Id = lowerId, + Version = lowerVersion, + IdVersion = $"{lowerId}.{lowerVersion}" }); } @@ -149,6 +160,26 @@ public string GetPackageIconDownloadUrl(string id, NuGetVersion version) }); } + public string GetServiceIndexV2Url() + { + return _linkGenerator.GetUriByRouteValues( + _httpContextAccessor.HttpContext, + Routes.V2IndexRouteName, + values: null); + } + + public string GetPackageVersionV2Url(Package package) + { + return _linkGenerator.GetUriByRouteValues( + _httpContextAccessor.HttpContext, + Routes.V2PackageVersionRouteName, + values: new + { + Id = package.Id, + Version = package.NormalizedVersionString + }); + } + private string AbsoluteUrl(string relativePath) { var request = _httpContextAccessor.HttpContext.Request; diff --git a/src/BaGet.Web/Controllers/V2ApiController.cs b/src/BaGet.Web/Controllers/V2ApiController.cs new file mode 100644 index 000000000..60d42d022 --- /dev/null +++ b/src/BaGet.Web/Controllers/V2ApiController.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using BaGet.Core; +using BaGet.Protocol; +using Microsoft.AspNetCore.Mvc; +using NuGet.Versioning; + +namespace BaGet.Web +{ + /// + /// Controller that implements the legacy NuGet V2 APIs. + /// + [Produces("application/xml")] + public class V2ApiController : Controller + { + private readonly ISearchService _search; + private readonly IMirrorService _mirror; + private readonly IV2Builder _builder; + + public V2ApiController( + ISearchService search, + IMirrorService mirror, + IV2Builder builder) + { + _search = search ?? throw new ArgumentNullException(nameof(search)); + _mirror = mirror ?? throw new ArgumentNullException(nameof(mirror)); + _builder = builder ?? throw new ArgumentNullException(nameof(builder)); + } + + public XElement Index() => _builder.BuildIndex(); + + public async Task List( + [FromQuery(Name = "$skip")] int skip = 0, + [FromQuery(Name = "$top")] int top = 20, + [FromQuery(Name = "$orderby")] string orderBy = null, + CancellationToken cancellationToken = default) + { + // TODO: Order by + var search = new SearchRequest + { + Skip = skip, + Take = top, + IncludePrerelease = true, + IncludeSemVer2 = true, + }; + + var response = await _search.SearchAsync(search, cancellationToken); + + // TODO: Undo + var packages = response + .Data + .Select(r => new Package + { + Id = r.PackageId, + Authors = r.Authors.ToArray(), + Description = r.Description, + Downloads = r.TotalDownloads, + Language = "English", + MinClientVersion = "1.2.3", + Published = DateTime.Now.AddDays(-1), + Summary = r.Summary, + + IconUrl = new Uri(r.IconUrl), + LicenseUrl = new Uri(r.LicenseUrl), + ProjectUrl = new Uri(r.ProjectUrl), + RepositoryUrl = new Uri(r.ProjectUrl), // TODO + + Tags = r.Tags.ToArray(), + + Version = r.ParseVersion(), + + Dependencies = new List() + }) + .ToList(); + + return _builder.BuildPackages(packages); + } + + public async Task Search( + string searchTerm, + string targetFramework, + bool includePrerelease = true, + CancellationToken cancellationToken = default) + { + searchTerm = searchTerm?.Trim('\'') ?? ""; + targetFramework = targetFramework?.Trim('\'') ?? ""; + + // TODO: Order by + var search = new SearchRequest + { + Skip = 0, + Take = 20, + IncludePrerelease = includePrerelease, + IncludeSemVer2 = true, + Query = searchTerm + }; + + var response = await _search.SearchAsync(search, cancellationToken); + + // TODO: Undo + var packages = response + .Data + .Select(r => new Package + { + Id = r.PackageId, + Authors = r.Authors.ToArray(), + Description = r.Description, + Downloads = r.TotalDownloads, + Language = "English", + MinClientVersion = "1.2.3", + Published = DateTime.Now.AddDays(-1), + Summary = r.Summary, + + IconUrl = new Uri(r.IconUrl), + LicenseUrl = new Uri(r.LicenseUrl), + ProjectUrl = new Uri(r.ProjectUrl), + RepositoryUrl = new Uri(r.ProjectUrl), // TODO + + Tags = r.Tags.ToArray(), + + Version = r.ParseVersion(), + + Dependencies = new List() + }) + .ToList(); + + return _builder.BuildPackages(packages); + } + + public async Task> Package(string id, CancellationToken cancellationToken) + { + id = id?.Trim('\''); + + var packages = await _mirror.FindPackagesAsync(id, cancellationToken); + if (!packages.Any()) + { + return NotFound(); + } + + return _builder.BuildPackages(packages); + } + + public async Task> PackageVersion(string id, string version, CancellationToken cancellationToken) + { + if (!NuGetVersion.TryParse(version, out var nugetVersion)) + { + return BadRequest(); + } + + var package = await _mirror.FindPackageOrNullAsync(id, nugetVersion, cancellationToken); + if (package == null) + { + return NotFound(); + } + + return _builder.BuildPackage(package); + } + } +} diff --git a/src/BaGet.Web/Extensions/IServiceCollectionExtensions.cs b/src/BaGet.Web/Extensions/IServiceCollectionExtensions.cs index 39964850d..36dbdc95e 100644 --- a/src/BaGet.Web/Extensions/IServiceCollectionExtensions.cs +++ b/src/BaGet.Web/Extensions/IServiceCollectionExtensions.cs @@ -17,6 +17,7 @@ public static IServiceCollection AddBaGetWebApplication( .AddControllers() .AddApplicationPart(typeof(PackageContentController).Assembly) .SetCompatibilityVersion(CompatibilityVersion.Version_3_0) + .AddXmlSerializerFormatters() .AddJsonOptions(options => { options.JsonSerializerOptions.IgnoreNullValues = true; diff --git a/src/BaGet.Web/Pages/Package.cshtml.cs b/src/BaGet.Web/Pages/Package.cshtml.cs index 8841648e7..b6aed0961 100644 --- a/src/BaGet.Web/Pages/Package.cshtml.cs +++ b/src/BaGet.Web/Pages/Package.cshtml.cs @@ -107,7 +107,7 @@ public async Task OnGetAsync(string id, string version, CancellationToken cancel ? _url.GetPackageIconDownloadUrl(Package.Id, packageVersion) : Package.IconUrlString; LicenseUrl = Package.LicenseUrlString; - PackageDownloadUrl = _url.GetPackageDownloadUrl(Package.Id, packageVersion); + PackageDownloadUrl = _url.GetPackageDownloadUrl(Package); } private IReadOnlyList ToDependencyGroups(Package package) diff --git a/src/BaGet.Web/Pages/Upload.cshtml b/src/BaGet.Web/Pages/Upload.cshtml index 06c76a53c..09cf91801 100644 --- a/src/BaGet.Web/Pages/Upload.cshtml +++ b/src/BaGet.Web/Pages/Upload.cshtml @@ -6,7 +6,7 @@ var baseUrl = Url.PageLink("/Index"); var publishUrl = _url.GetPackagePublishResourceUrl(); - var serviceIndexUrl = _url.GetServiceIndexUrl(); + var serviceIndexUrl = _url.GetServiceIndexV3Url(); }
diff --git a/src/BaGet.Web/Routes.cs b/src/BaGet.Web/Routes.cs index ff31ad389..7bd6e2536 100644 --- a/src/BaGet.Web/Routes.cs +++ b/src/BaGet.Web/Routes.cs @@ -1,8 +1,8 @@ namespace BaGet.Web { - public class Routes + public static class Routes { - public const string IndexRouteName = "index"; + public const string V3IndexRouteName = "index"; public const string UploadPackageRouteName = "upload-package"; public const string UploadSymbolRouteName = "upload-symbol"; public const string DeleteRouteName = "delete"; @@ -19,5 +19,11 @@ public class Routes public const string PackageDownloadIconRouteName = "package-download-icon"; public const string SymbolDownloadRouteName = "symbol-download"; public const string PrefixedSymbolDownloadRouteName = "prefixed-symbol-download"; + + public const string V2IndexRouteName = "v2-service-index"; + public const string V2ListRouteName = "v2-list"; + public const string V2SearchRouteName = "v2-search"; + public const string V2PackageRouteName = "v2-package"; + public const string V2PackageVersionRouteName = "v2-package-version"; } } From e5ed7e1fa92b3a4f212218239cb73edb7c99a9b3 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 12 Dec 2021 14:40:01 -0800 Subject: [PATCH 2/7] Tweaks --- src/BaGet.Core/IUrlGenerator.cs | 14 +++++++------- src/BaGet.Core/V2/IV2Builder.cs | 1 + src/BaGet.Core/V2/V2Builder.cs | 4 ++-- src/BaGet.Web/BaGetUrlGenerator.cs | 4 ++-- src/BaGet.Web/Pages/Upload.cshtml | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/BaGet.Core/IUrlGenerator.cs b/src/BaGet.Core/IUrlGenerator.cs index 905c6917e..9115890ad 100644 --- a/src/BaGet.Core/IUrlGenerator.cs +++ b/src/BaGet.Core/IUrlGenerator.cs @@ -11,7 +11,12 @@ public interface IUrlGenerator /// Get the URL for the package source (also known as the "service index"). /// See: https://docs.microsoft.com/en-us/nuget/api/service-index ///
- string GetServiceIndexV3Url(); + string GetServiceIndexUrl(); + + /// + /// Get the URL for the package source that implements the legacy NuGet V2 API. + /// + string GetServiceIndexV2Url(); /// /// Get the URL for the root of the package content resource. @@ -109,15 +114,10 @@ public interface IUrlGenerator /// The package's version string GetPackageIconDownloadUrl(string id, NuGetVersion version); - /// - /// Get the URL for the package source that implements the legacy NuGet V2 API. - /// - string GetServiceIndexV2Url(); - /// /// Get the URL for the metadata of a single package version. /// /// The package to lookup - string GetPackageVersionV2Url(Package package); + string GetPackageMetadataV2Url(Package package); } } diff --git a/src/BaGet.Core/V2/IV2Builder.cs b/src/BaGet.Core/V2/IV2Builder.cs index 2689b6706..48a673ce3 100644 --- a/src/BaGet.Core/V2/IV2Builder.cs +++ b/src/BaGet.Core/V2/IV2Builder.cs @@ -3,6 +3,7 @@ namespace BaGet.Core { + // TODO: comments public interface IV2Builder { XElement BuildIndex(); diff --git a/src/BaGet.Core/V2/V2Builder.cs b/src/BaGet.Core/V2/V2Builder.cs index 245036175..13c8214d9 100644 --- a/src/BaGet.Core/V2/V2Builder.cs +++ b/src/BaGet.Core/V2/V2Builder.cs @@ -45,7 +45,7 @@ public XElement BuildPackages(IReadOnlyList packages) packages.Select(package => { - var packageV2Url = _url.GetPackageVersionV2Url(package); + var packageV2Url = _url.GetPackageMetadataV2Url(package); var downloadUrl = _url.GetPackageDownloadUrl(package); return new XElement( @@ -69,7 +69,7 @@ public XElement BuildPackage(Package package) { // See: https://joelverhagen.github.io/NuGetUndocs/#endpoint-get-a-single-package var serviceIndex = _url.GetServiceIndexV2Url(); - var packageV2Url = _url.GetPackageVersionV2Url(package); + var packageV2Url = _url.GetPackageMetadataV2Url(package); var downloadUrl = _url.GetPackageDownloadUrl(package); return new XElement( diff --git a/src/BaGet.Web/BaGetUrlGenerator.cs b/src/BaGet.Web/BaGetUrlGenerator.cs index 66d2dae63..554d65c2a 100644 --- a/src/BaGet.Web/BaGetUrlGenerator.cs +++ b/src/BaGet.Web/BaGetUrlGenerator.cs @@ -18,7 +18,7 @@ public BaGetUrlGenerator(IHttpContextAccessor httpContextAccessor, LinkGenerator _linkGenerator = linkGenerator ?? throw new ArgumentNullException(nameof(linkGenerator)); } - public string GetServiceIndexV3Url() + public string GetServiceIndexUrl() { return _linkGenerator.GetUriByRouteValues( _httpContextAccessor.HttpContext, @@ -168,7 +168,7 @@ public string GetServiceIndexV2Url() values: null); } - public string GetPackageVersionV2Url(Package package) + public string GetPackageMetadataV2Url(Package package) { return _linkGenerator.GetUriByRouteValues( _httpContextAccessor.HttpContext, diff --git a/src/BaGet.Web/Pages/Upload.cshtml b/src/BaGet.Web/Pages/Upload.cshtml index 09cf91801..06c76a53c 100644 --- a/src/BaGet.Web/Pages/Upload.cshtml +++ b/src/BaGet.Web/Pages/Upload.cshtml @@ -6,7 +6,7 @@ var baseUrl = Url.PageLink("/Index"); var publishUrl = _url.GetPackagePublishResourceUrl(); - var serviceIndexUrl = _url.GetServiceIndexV3Url(); + var serviceIndexUrl = _url.GetServiceIndexUrl(); }
From 4a5091dcce9e30ca3aade19269bda6be3cfb1420 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 12 Dec 2021 14:41:44 -0800 Subject: [PATCH 3/7] Update --- src/BaGet.Web/Controllers/V2ApiController.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/BaGet.Web/Controllers/V2ApiController.cs b/src/BaGet.Web/Controllers/V2ApiController.cs index 60d42d022..68eded60f 100644 --- a/src/BaGet.Web/Controllers/V2ApiController.cs +++ b/src/BaGet.Web/Controllers/V2ApiController.cs @@ -17,17 +17,17 @@ namespace BaGet.Web [Produces("application/xml")] public class V2ApiController : Controller { + private readonly IPackageService _packages; private readonly ISearchService _search; - private readonly IMirrorService _mirror; private readonly IV2Builder _builder; public V2ApiController( + IPackageService packages, ISearchService search, - IMirrorService mirror, IV2Builder builder) { + _packages = packages ?? throw new ArgumentNullException(nameof(packages)); _search = search ?? throw new ArgumentNullException(nameof(search)); - _mirror = mirror ?? throw new ArgumentNullException(nameof(mirror)); _builder = builder ?? throw new ArgumentNullException(nameof(builder)); } @@ -135,7 +135,7 @@ public async Task> Package(string id, CancellationToken c { id = id?.Trim('\''); - var packages = await _mirror.FindPackagesAsync(id, cancellationToken); + var packages = await _packages.FindPackagesAsync(id, cancellationToken); if (!packages.Any()) { return NotFound(); @@ -151,7 +151,7 @@ public async Task> PackageVersion(string id, string versi return BadRequest(); } - var package = await _mirror.FindPackageOrNullAsync(id, nugetVersion, cancellationToken); + var package = await _packages.FindPackageOrNullAsync(id, nugetVersion, cancellationToken); if (package == null) { return NotFound(); From cfe531547643dfd0dc131cfd83c85d3144dfbaad Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 12 Dec 2021 16:39:44 -0800 Subject: [PATCH 4/7] Improve output --- .../DependencyInjectionExtensions.cs | 1 + src/BaGet.Core/V2/V2Builder.cs | 59 +++++++++++-------- 2 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/BaGet.Core/Extensions/DependencyInjectionExtensions.cs b/src/BaGet.Core/Extensions/DependencyInjectionExtensions.cs index a56c41eab..e9d193df7 100644 --- a/src/BaGet.Core/Extensions/DependencyInjectionExtensions.cs +++ b/src/BaGet.Core/Extensions/DependencyInjectionExtensions.cs @@ -75,6 +75,7 @@ private static void AddBaGetServices(this IServiceCollection services) services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/BaGet.Core/V2/V2Builder.cs b/src/BaGet.Core/V2/V2Builder.cs index 13c8214d9..bf0b197a0 100644 --- a/src/BaGet.Core/V2/V2Builder.cs +++ b/src/BaGet.Core/V2/V2Builder.cs @@ -79,14 +79,15 @@ public XElement BuildPackage(Package package) new XAttribute(N.d, NS.d), new XAttribute(N.georss, NS.georss), new XAttribute(N.gml, NS.gml), - new XElement(N.id, packageV2Url), - new XElement(N.title, package.Title), + new XElement(N.id, packageV2Url), new XElement( N.content, new XAttribute("type", "application/zip"), new XAttribute("src", downloadUrl) ), + new XElement(N.summary, package.Summary), + new XElement(N.title, package.Title), BuildAuthor(package), BuildProperties(package) @@ -98,10 +99,6 @@ private XElement BuildProperties(Package package) // See: https://joelverhagen.github.io/NuGetUndocs/#package-entity return new XElement( N.m_properties, - new XElement(N.d_Id, package.Id), - new XElement(N.d_Title, package.Title), - new XElement(N.d_Version, package.OriginalVersionString), - new XElement(N.d_NormalizedVersion, package.NormalizedVersionString), new XElement(N.d_Authors, string.Join(", ", package.Authors)), new XElement(N.d_Copyright, ""), // TODO new XElement(N.d_Description, package.Description), @@ -109,16 +106,24 @@ private XElement BuildProperties(Package package) N.d_DownloadCount, new XAttribute(N.m_type, "Edm.Int32"), package.Downloads), + new XElement(N.d_IconUrl, package.IconUrl), // TODO, URL logic + new XElement(N.d_Id, package.Id), + new XElement(N.d_Language, package.Language), new XElement(N.d_LastEdited, package.Published), - new XElement(N.d_Published, package.Published), + new XElement(N.d_LicenseUrl, package.LicenseUrl), // TODO + new XElement(N.d_MinClientVersion, package.MinClientVersion), + new XElement(N.d_NormalizedVersion, package.NormalizedVersionString), new XElement(N.d_PackageHash, ""), new XElement(N.d_PackageHashAlgorithm, ""), new XElement(N.d_PackageSize, 0), new XElement(N.d_ProjectUrl, package.ProjectUrl), - new XElement(N.d_IconUrl, package.IconUrl), // TODO, URL logic - new XElement(N.d_LicenseUrl, package.LicenseUrl), // TODO - new XElement(N.d_Tags, string.Join(", ", package.Tags)), + new XElement(N.d_Published, package.Published), + new XElement(N.d_ReleaseNotes, package.ReleaseNotes), new XElement(N.d_RequireLicenseAcceptance, package.RequireLicenseAcceptance), + new XElement(N.d_Summary, package.Summary), + new XElement(N.d_Tags, string.Join(" ", package.Tags)), + new XElement(N.d_Title, package.Title), + new XElement(N.d_Version, package.OriginalVersionString), BuildDependencies(package) ); @@ -126,10 +131,9 @@ private XElement BuildProperties(Package package) private XElement BuildAuthor(Package package) { - // TODO: No authors? return new XElement( N.author, - package.Authors.Select(author => new XElement(N.name, author)) + new XElement(N.name, string.Join(", ", package.Authors)) ); } @@ -164,6 +168,7 @@ private bool IsFrameworkDependency(PackageDependency dependency) private static class NS { public static readonly XNamespace xmlns = "http://www.w3.org/2005/Atom"; + // TODO: Remove? //public static readonly XNamespace baze = "https://www.nuget.org/api/v2/curated-feeds/microsoftdotnet"; public static readonly XNamespace m = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; public static readonly XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices"; @@ -181,33 +186,37 @@ private static class N public static readonly XName link = NS.xmlns + "link"; public static readonly XName id = NS.xmlns + "id"; public static readonly XName content = NS.xmlns + "content"; + public static readonly XName summary = NS.xmlns + "summary"; public static readonly XName m_count = NS.m + "count"; public static readonly XName m_properties = NS.m + "properties"; public static readonly XName m_type = NS.m + "type"; - public static readonly XName d_Id = NS.d + "Id"; - public static readonly XName d_Title = NS.d + "Title"; - public static readonly XName d_Version = NS.d + "Version"; - public static readonly XName d_NormalizedVersion = NS.d + "NormalizedVersion"; public static readonly XName d_Authors = NS.d + "Authors"; public static readonly XName d_Copyright = NS.d + "Copyright"; + public static readonly XName d_Created = NS.d + "Created"; public static readonly XName d_Dependencies = NS.d + "Dependencies"; public static readonly XName d_Description = NS.d + "Description"; - public static readonly XName d_IconUrl = NS.d + "IconUrl"; - public static readonly XName d_LicenseUrl = NS.d + "LicenseUrl"; - public static readonly XName d_ProjectUrl = NS.d + "ProjectUrl"; - public static readonly XName d_Tags = NS.d + "Tags"; - public static readonly XName d_ReportAbuseUrl = NS.d + "ReportAbuseUrl"; - public static readonly XName d_RequireLicenseAcceptance = NS.d + "RequireLicenseAcceptance"; public static readonly XName d_DownloadCount = NS.d + "DownloadCount"; - public static readonly XName d_Created = NS.d + "Created"; + public static readonly XName d_IconUrl = NS.d + "IconUrl"; + public static readonly XName d_Id = NS.d + "Id"; + public static readonly XName d_Language = NS.d + "Language"; public static readonly XName d_LastEdited = NS.d + "LastEdited"; - public static readonly XName d_Published = NS.d + "Published"; + public static readonly XName d_LicenseUrl = NS.d + "LicenseUrl"; + public static readonly XName d_MinClientVersion = NS.d + "MinClientVersion"; + public static readonly XName d_NormalizedVersion = NS.d + "NormalizedVersion"; public static readonly XName d_PackageHash = NS.d + "PackageHash"; public static readonly XName d_PackageHashAlgorithm = NS.d + "PackageHashAlgorithm"; - public static readonly XName d_MinClientVersion = NS.d + "MinClientVersion"; public static readonly XName d_PackageSize = NS.d + "PackageSize"; + public static readonly XName d_ProjectUrl = NS.d + "ProjectUrl"; + public static readonly XName d_Published = NS.d + "Published"; + public static readonly XName d_ReleaseNotes = NS.d + "ReleaseNotes"; + public static readonly XName d_ReportAbuseUrl = NS.d + "ReportAbuseUrl"; + public static readonly XName d_RequireLicenseAcceptance = NS.d + "RequireLicenseAcceptance"; + public static readonly XName d_Summary = NS.d + "Summary"; + public static readonly XName d_Tags = NS.d + "Tags"; + public static readonly XName d_Title = NS.d + "Title"; + public static readonly XName d_Version = NS.d + "Version"; public static readonly XName baze = XNamespace.Xmlns + "base"; public static readonly XName m = XNamespace.Xmlns + "m"; From c5ed3b5d4cab373fe0a992bfa79d881be314a437 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 12 Dec 2021 17:01:28 -0800 Subject: [PATCH 5/7] Work --- src/BaGet.Core/V2/V2Builder.cs | 5 ++++- src/BaGet.Web/Controllers/V2ApiController.cs | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/BaGet.Core/V2/V2Builder.cs b/src/BaGet.Core/V2/V2Builder.cs index bf0b197a0..05dbc7e68 100644 --- a/src/BaGet.Core/V2/V2Builder.cs +++ b/src/BaGet.Core/V2/V2Builder.cs @@ -34,6 +34,7 @@ public XElement BuildPackages(IReadOnlyList packages) var serviceIndex = _url.GetServiceIndexV2Url(); // TODO: Add to top + // TODO: nuget.org adds `m:null="true"` attribute to elements with no value. Is that necessary? return new XElement( N.feed, new XAttribute(N.baze, XNamespace.Get(serviceIndex)), @@ -51,7 +52,7 @@ public XElement BuildPackages(IReadOnlyList packages) return new XElement( N.entry, new XElement(N.id, packageV2Url), - new XElement(N.title, package.Title), + new XElement(N.title, package.Id), new XElement( N.content, new XAttribute("type", "application/zip"), @@ -108,6 +109,7 @@ private XElement BuildProperties(Package package) package.Downloads), new XElement(N.d_IconUrl, package.IconUrl), // TODO, URL logic new XElement(N.d_Id, package.Id), + new XElement(N.d_IsPrerelease, package.Version.IsPrerelease), new XElement(N.d_Language, package.Language), new XElement(N.d_LastEdited, package.Published), new XElement(N.d_LicenseUrl, package.LicenseUrl), // TODO @@ -200,6 +202,7 @@ private static class N public static readonly XName d_DownloadCount = NS.d + "DownloadCount"; public static readonly XName d_IconUrl = NS.d + "IconUrl"; public static readonly XName d_Id = NS.d + "Id"; + public static readonly XName d_IsPrerelease = NS.d + "IsPrerelease"; public static readonly XName d_Language = NS.d + "Language"; public static readonly XName d_LastEdited = NS.d + "LastEdited"; public static readonly XName d_LicenseUrl = NS.d + "LicenseUrl"; diff --git a/src/BaGet.Web/Controllers/V2ApiController.cs b/src/BaGet.Web/Controllers/V2ApiController.cs index 68eded60f..ddd0d6390 100644 --- a/src/BaGet.Web/Controllers/V2ApiController.cs +++ b/src/BaGet.Web/Controllers/V2ApiController.cs @@ -124,6 +124,7 @@ public async Task Search( Version = r.ParseVersion(), + // TODO: Need to load depedencies! Dependencies = new List() }) .ToList(); From 67065c34de06f926895209a6a37687d0cf0fbbc7 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Sun, 12 Dec 2021 17:03:00 -0800 Subject: [PATCH 6/7] Add TODOs --- src/BaGet.Web/Controllers/V2ApiController.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/BaGet.Web/Controllers/V2ApiController.cs b/src/BaGet.Web/Controllers/V2ApiController.cs index ddd0d6390..db04c0cc9 100644 --- a/src/BaGet.Web/Controllers/V2ApiController.cs +++ b/src/BaGet.Web/Controllers/V2ApiController.cs @@ -110,9 +110,9 @@ public async Task Search( Authors = r.Authors.ToArray(), Description = r.Description, Downloads = r.TotalDownloads, - Language = "English", - MinClientVersion = "1.2.3", - Published = DateTime.Now.AddDays(-1), + Language = "English", // TODO + MinClientVersion = "1.2.3", // TODO + Published = DateTime.Now.AddDays(-1), // TODO Summary = r.Summary, IconUrl = new Uri(r.IconUrl), From 6c2611415a2ba12c596eb83d1af0376226b3f721 Mon Sep 17 00:00:00 2001 From: Loic Sharma Date: Mon, 13 Dec 2021 21:34:56 -0800 Subject: [PATCH 7/7] Add upstream to V2 integration test --- .../Upstream/Clients/V2UpstreamClient.cs | 28 ++++++-------- src/BaGet.Web/Controllers/V2ApiController.cs | 1 + tests/BaGet.Tests/ApiIntegrationTests.cs | 12 +++--- .../BaGetClientIntegrationTests.cs | 4 +- tests/BaGet.Tests/MirrorIntegrationTests.cs | 34 ++++++++++++++--- .../NuGetClientIntegrationTests.cs | 11 ++---- tests/BaGet.Tests/Support/BaGetApplication.cs | 36 ++++++++++++++---- ... => TestableHttpSourceResourceProvider.cs} | 19 ++++----- .../Support/TestableSourceRepository.cs | 25 ++++++++++++ .../BaGet.Tests/TestData/TestData.1.2.3.nupkg | Bin 3172 -> 3226 bytes 10 files changed, 116 insertions(+), 54 deletions(-) rename tests/BaGet.Tests/Support/{HttpSourceResourceProviderTestHost.cs => TestableHttpSourceResourceProvider.cs} (73%) create mode 100644 tests/BaGet.Tests/Support/TestableSourceRepository.cs diff --git a/src/BaGet.Core/Upstream/Clients/V2UpstreamClient.cs b/src/BaGet.Core/Upstream/Clients/V2UpstreamClient.cs index 763aed6cb..e9e3aa989 100644 --- a/src/BaGet.Core/Upstream/Clients/V2UpstreamClient.cs +++ b/src/BaGet.Core/Upstream/Clients/V2UpstreamClient.cs @@ -23,30 +23,26 @@ namespace BaGet.Core ///
public class V2UpstreamClient : IUpstreamClient, IDisposable { - private readonly SourceCacheContext _cache; private readonly SourceRepository _repository; - private readonly INuGetLogger _ngLogger; private readonly ILogger _logger; + private readonly SourceCacheContext _cache = new SourceCacheContext(); + private readonly INuGetLogger _ngLogger = NullLogger.Instance; + public V2UpstreamClient( IOptionsSnapshot options, ILogger logger) { - if (options is null) - { - throw new ArgumentNullException(nameof(options)); - } - - if (options.Value?.PackageSource?.AbsolutePath == null) - { - throw new ArgumentException("No mirror package source has been set."); - } + var source = new PackageSource(options.Value.PackageSource.AbsoluteUri); + _repository = Repository.Factory.GetCoreV2(source); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } - _ngLogger = NullLogger.Instance; - _cache = new SourceCacheContext(); - _repository = Repository.Factory.GetCoreV2(new PackageSource(options.Value.PackageSource.AbsoluteUri)); + public V2UpstreamClient(SourceRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public async Task> ListPackageVersionsAsync(string id, CancellationToken cancellationToken) @@ -138,9 +134,9 @@ private Package ToPackage(IPackageSearchMetadata package) Description = package.Description, Downloads = 0, HasReadme = false, - Language = null, + Language = string.Empty, Listed = package.IsListed, - MinClientVersion = null, + MinClientVersion = string.Empty, Published = package.Published?.UtcDateTime ?? DateTime.MinValue, RequireLicenseAcceptance = package.RequireLicenseAcceptance, Summary = package.Summary, diff --git a/src/BaGet.Web/Controllers/V2ApiController.cs b/src/BaGet.Web/Controllers/V2ApiController.cs index db04c0cc9..6067e9b7a 100644 --- a/src/BaGet.Web/Controllers/V2ApiController.cs +++ b/src/BaGet.Web/Controllers/V2ApiController.cs @@ -134,6 +134,7 @@ public async Task Search( public async Task> Package(string id, CancellationToken cancellationToken) { + // TODO: Accept semVerLevel=2.0.0 query parameter id = id?.Trim('\''); var packages = await _packages.FindPackagesAsync(id, cancellationToken); diff --git a/tests/BaGet.Tests/ApiIntegrationTests.cs b/tests/BaGet.Tests/ApiIntegrationTests.cs index 547f51b79..7aaab4b27 100644 --- a/tests/BaGet.Tests/ApiIntegrationTests.cs +++ b/tests/BaGet.Tests/ApiIntegrationTests.cs @@ -55,7 +55,7 @@ public async Task SearchReturnsOk() { ""id"": ""TestData"", ""version"": ""1.2.3"", - ""description"": ""Test description"", + ""description"": ""Hello world"", ""authors"": [ ""Test author"" ], @@ -63,9 +63,9 @@ public async Task SearchReturnsOk() ""licenseUrl"": """", ""projectUrl"": """", ""registration"": ""http://localhost/v3/registration/testdata/index.json"", - ""summary"": """", + ""summary"": ""Hello world"", ""tags"": [], - ""title"": """", + ""title"": ""TestData"", ""totalDownloads"": 0, ""versions"": [ { @@ -277,7 +277,7 @@ public async Task PackageMetadataReturnsOk() ""dependencies"": [] } ], - ""description"": ""Test description"", + ""description"": ""Hello world"", ""iconUrl"": """", ""language"": """", ""licenseUrl"": """", @@ -287,9 +287,9 @@ public async Task PackageMetadataReturnsOk() ""projectUrl"": """", ""published"": ""2020-01-01T00:00:00Z"", ""requireLicenseAcceptance"": false, - ""summary"": """", + ""summary"": ""Hello world"", ""tags"": [], - ""title"": """" + ""title"": ""TestData"" } } ] diff --git a/tests/BaGet.Tests/BaGetClientIntegrationTests.cs b/tests/BaGet.Tests/BaGetClientIntegrationTests.cs index c73aa376a..75c8fdaf6 100644 --- a/tests/BaGet.Tests/BaGetClientIntegrationTests.cs +++ b/tests/BaGet.Tests/BaGetClientIntegrationTests.cs @@ -65,7 +65,7 @@ public async Task SearchReturnsResults() Assert.Equal("TestData", result.PackageId); Assert.Equal("1.2.3", result.Version); - Assert.Equal("Test description", result.Description); + Assert.Equal("Hello world", result.Description); Assert.Equal("Test author", author); Assert.Equal(0, result.TotalDownloads); @@ -210,7 +210,7 @@ public async Task PackageMetadataReturnsOk() Assert.Equal("TestData", package.PackageId); Assert.Equal("1.2.3", package.Version); - Assert.Equal("Test description", package.Description); + Assert.Equal("Hello world", package.Description); Assert.Equal("Test author", package.Authors); Assert.True(package.Listed); } diff --git a/tests/BaGet.Tests/MirrorIntegrationTests.cs b/tests/BaGet.Tests/MirrorIntegrationTests.cs index 8f6141772..f12257f84 100644 --- a/tests/BaGet.Tests/MirrorIntegrationTests.cs +++ b/tests/BaGet.Tests/MirrorIntegrationTests.cs @@ -8,17 +8,39 @@ namespace BaGet.Tests { - public class MirrorIntegrationTests : IDisposable + public class V2UpstreamMirrorIntegrationTests : MirrorIntegrationTests + { + public V2UpstreamMirrorIntegrationTests(ITestOutputHelper output) + : base(output, v2Upstream: true) + { + } + } + + public class V3UpstreamMirrorIntegrationTests : MirrorIntegrationTests + { + public V3UpstreamMirrorIntegrationTests(ITestOutputHelper output) + : base(output, v2Upstream: false) + { + } + } + + public abstract class MirrorIntegrationTests : IDisposable { private readonly BaGetApplication _upstream; private readonly BaGetApplication _downstream; private readonly HttpClient _downstreamClient; private readonly Stream _packageStream; - public MirrorIntegrationTests(ITestOutputHelper output) + protected MirrorIntegrationTests(ITestOutputHelper output, bool v2Upstream) { _upstream = new BaGetApplication(output); - _downstream = new BaGetApplication(output, _upstream.CreateClient()); + _downstream = new BaGetApplication( + output, + new BaGetApplicationOptions + { + UpstreamClient = _upstream.CreateClient(), + EnableLegacyUpstream = v2Upstream, + }); _downstreamClient = _downstream.CreateClient(); _packageStream = TestResources.GetResourceStream(TestResources.Package); @@ -120,7 +142,7 @@ public async Task PackageMetadataIncludesUpstream() ""dependencies"": [] } ], - ""description"": ""Test description"", + ""description"": ""Hello world"", ""iconUrl"": """", ""language"": """", ""licenseUrl"": """", @@ -130,9 +152,9 @@ public async Task PackageMetadataIncludesUpstream() ""projectUrl"": """", ""published"": ""2020-01-01T00:00:00Z"", ""requireLicenseAcceptance"": false, - ""summary"": """", + ""summary"": ""Hello world"", ""tags"": [], - ""title"": """" + ""title"": ""TestData"" } } ] diff --git a/tests/BaGet.Tests/NuGetClientIntegrationTests.cs b/tests/BaGet.Tests/NuGetClientIntegrationTests.cs index e394ac95d..a1b9f1b73 100644 --- a/tests/BaGet.Tests/NuGetClientIntegrationTests.cs +++ b/tests/BaGet.Tests/NuGetClientIntegrationTests.cs @@ -35,13 +35,8 @@ public NuGetClientIntegrationTests(ITestOutputHelper output) _packageStream = TestResources.GetResourceStream(TestResources.Package); var sourceUri = new Uri(_app.Server.BaseAddress, "v3/index.json"); - var packageSource = new PackageSource(sourceUri.AbsoluteUri); - var providers = new List>(); - providers.Add(new Lazy(() => new HttpSourceResourceProviderTestHost(_client))); - providers.AddRange(Repository.Provider.GetCoreV3()); - - _repository = new SourceRepository(packageSource, providers); + _repository = TestableSourceRepository.Build(sourceUri, _client); _cache = new SourceCacheContext { NoCache = true, MaxAge = new DateTimeOffset(), DirectDownload = true }; _logger = NuGet.Common.NullLogger.Instance; _cancellationToken = CancellationToken.None; @@ -82,7 +77,7 @@ public async Task SearchReturnsResults() Assert.Equal("TestData", result.Identity.Id); Assert.Equal("1.2.3", result.Identity.Version.ToNormalizedString()); - Assert.Equal("Test description", result.Description); + Assert.Equal("Hello world", result.Description); Assert.Equal("Test author", result.Authors); Assert.Equal(0, result.DownloadCount); @@ -234,7 +229,7 @@ public async Task PackageMetadataReturnsOk() Assert.Equal("TestData", package.Identity.Id); Assert.Equal("1.2.3", package.Identity.Version.ToNormalizedString()); - Assert.Equal("Test description", package.Description); + Assert.Equal("Hello world", package.Description); Assert.Equal("Test author", package.Authors); Assert.True(package.IsListed); } diff --git a/tests/BaGet.Tests/Support/BaGetApplication.cs b/tests/BaGet.Tests/Support/BaGetApplication.cs index 9886e1bd0..1c0d96337 100644 --- a/tests/BaGet.Tests/Support/BaGetApplication.cs +++ b/tests/BaGet.Tests/Support/BaGetApplication.cs @@ -18,15 +18,28 @@ namespace BaGet.Tests { + public class BaGetApplicationOptions + { + /// + /// Null if upstreaming should be disabled. + /// + public HttpClient UpstreamClient { get; set; } + + /// + /// True if the upstream uses NuGet's V2 protocol. + /// + public bool EnableLegacyUpstream { get; set; } + } + public class BaGetApplication : WebApplicationFactory { private readonly ITestOutputHelper _output; - private readonly HttpClient _upstreamClient; + private readonly BaGetApplicationOptions _options; - public BaGetApplication(ITestOutputHelper output, HttpClient upstreamClient = null) + public BaGetApplication(ITestOutputHelper output, BaGetApplicationOptions options = null) { _output = output ?? throw new ArgumentNullException(nameof(output)); - _upstreamClient = upstreamClient; + _options = options ?? new BaGetApplicationOptions(); } protected override void ConfigureWebHost(IWebHostBuilder builder) @@ -41,6 +54,10 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) Directory.CreateDirectory(tempPath); + var upstreamUrl = _options.EnableLegacyUpstream + ? "http://localhost/api/v2" + : "http://localhost/v3/index.json"; + builder .UseStartup() .UseEnvironment("Production") @@ -64,8 +81,9 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) { "Storage:Type", "FileSystem" }, { "Storage:Path", storagePath }, { "Search:Type", "Database" }, - { "Mirror:Enabled", _upstreamClient != null ? "true": "false" }, - { "Mirror:PackageSource", "http://localhost/v3/index.json" }, + { "Mirror:Enabled", _options.UpstreamClient != null ? "true": "false" }, + { "Mirror:Legacy", _options.EnableLegacyUpstream ? "true": "false" }, + { "Mirror:PackageSource", upstreamUrl }, }); }) .ConfigureServices((context, services) => @@ -77,9 +95,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) .Returns(DateTime.Parse("2020-01-01T00:00:00.000Z")); services.AddSingleton(time.Object); - if (_upstreamClient != null) + if (_options.UpstreamClient != null) { - services.AddSingleton(_upstreamClient); + services.AddSingleton(_options.UpstreamClient); + services.AddSingleton(provider => + new V2UpstreamClient( + TestableSourceRepository.Build(new Uri(upstreamUrl), _options.UpstreamClient), + provider.GetRequiredService>())); } // Setup the integration test database. diff --git a/tests/BaGet.Tests/Support/HttpSourceResourceProviderTestHost.cs b/tests/BaGet.Tests/Support/TestableHttpSourceResourceProvider.cs similarity index 73% rename from tests/BaGet.Tests/Support/HttpSourceResourceProviderTestHost.cs rename to tests/BaGet.Tests/Support/TestableHttpSourceResourceProvider.cs index 2420112a1..be1d95e44 100644 --- a/tests/BaGet.Tests/Support/HttpSourceResourceProviderTestHost.cs +++ b/tests/BaGet.Tests/Support/TestableHttpSourceResourceProvider.cs @@ -13,17 +13,18 @@ namespace BaGet.Tests /// /// Similar to official HttpSourceResourceProvider, but uses test host. /// - public class HttpSourceResourceProviderTestHost : ResourceProvider + public class TestableHttpSourceResourceProvider : ResourceProvider { // Only one HttpSource per source should exist. This is to reduce the number of TCP connections. private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); private readonly HttpClient _httpClient; - public HttpSourceResourceProviderTestHost(HttpClient httpClient) - : base(typeof(HttpSourceResource), - nameof(HttpSourceResource), - NuGetResourceProviderPositions.Last) + public TestableHttpSourceResourceProvider(HttpClient httpClient) + : base( + typeof(HttpSourceResource), + nameof(HttpSourceResource), + NuGetResourceProviderPositions.Last) { _httpClient = httpClient; } @@ -32,16 +33,16 @@ public override Task> TryCreate(SourceRepository sou { Debug.Assert(source.PackageSource.IsHttp, "HTTP source requested for a non-http source."); - HttpSourceResource curResource = null; + HttpSourceResource result = null; if (source.PackageSource.IsHttp) { - curResource = _cache.GetOrAdd( - source.PackageSource, + result = _cache.GetOrAdd( + source.PackageSource, packageSource => new HttpSourceResource(TestableHttpSource.Create(source, _httpClient))); } - return Task.FromResult(new Tuple(curResource != null, curResource)); + return Task.FromResult(new Tuple(result != null, result)); } } } diff --git a/tests/BaGet.Tests/Support/TestableSourceRepository.cs b/tests/BaGet.Tests/Support/TestableSourceRepository.cs new file mode 100644 index 000000000..4098c4c65 --- /dev/null +++ b/tests/BaGet.Tests/Support/TestableSourceRepository.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using NuGet.Configuration; +using NuGet.Protocol.Core.Types; + +namespace BaGet.Tests +{ + public static class TestableSourceRepository + { + public static SourceRepository Build(Uri source, HttpClient client) + { + var packageSource = new PackageSource(source.AbsoluteUri); + var providers = new List>(); + + var testableHttpProvider = new Lazy( + () => new TestableHttpSourceResourceProvider(client)); + + providers.Add(testableHttpProvider); + providers.AddRange(Repository.Provider.GetCoreV3()); + + return new SourceRepository(packageSource, providers); + } + } +} diff --git a/tests/BaGet.Tests/TestData/TestData.1.2.3.nupkg b/tests/BaGet.Tests/TestData/TestData.1.2.3.nupkg index 8ee37ba33fc1e0981087cb2cfbac5d9267edbd18..35cdf4d3b3d1504ad33defb5aae863f299993c28 100644 GIT binary patch delta 1324 zcmaDNF-x*Oz?+#xgn@y9gW==B?h0)3tIWM~;bQSS_uySK?Cs8!RZQA>XUXDv&Q6hu-mQ)OM>kj;}o2}w3|DvgDY&oue2$RpUoA)9v!>%%DmsqGr*y@6_ z8pmpOX2x%u{dSW}uGj2%$@GHo`+tNP821-4m$)RB zBxSt z8=!st%uBh3Gg5n(9kZUSd{oq2t;(X(n^`CAGwYc=#hxh_OsY3+n!>rzX4AG=Uj;?q z`f z@4Ni_qRO*v&e4Zkk`sT+99M~WF85P2yI*xrW~#$JOO+VGWhFCzsjqwR=69&Ac-ehm zfT0Fy-jX+)lbL3+vVhX(CJtssUJyf}R`p&gFpwiB3ve1L7#SKRS(+spm?c>zn;4m; zBpDhRo2H~$rWl!;C#M)%PEO>Ms26Cu?Qz9L_~-wm|tEIAUKb`2kWRfi3J>9&1 zWAiNjsUJ`DHcSqG|8Vh}OpU87smZZtFCORM6|dgc`rbo0jJfFK#Le%|wfWwU656G@ zWI~dfOu?3QQH}10qVGh8Z{YqtbMk}MVBR*)F#iQxRp*vQxqS<={_NHEa7Ay^{0saG zJ5I#iP_DY=HT9z4S?;nrw_N_?6I_0+O89HaHLJclrsZp^!KZf-d$;vBlt@bRYs*gf zetDI;32(ZIFZ+{yS8rHp@NRjrLvBLq?rUB)hYM4`C!Dg0&j0kX;!vvH&c>RW=neLf zhyHgY|M{zMRU95sj7%a7xJwYAG6Vb>UzD0ttgix9<{V=xsd&V`^2YHg1vPUrd-%p%j&DRP~_I*4c7P1ZCHJFx8+3c zwiR>VW=0x#UGOr`wEq5O&XuOiXMcT6EIlyUqf%Y2ervzV?3_0*4ORcM2s{<+?P&d+ z@!4f(g3BqRGY?v8gpChP+Q@cDw>eEqu{_48rs9vIX%w6IT(8!WXIy7q8f{8B5M^*E ztJN^jsKX%nl_pQ*J6EArTlfA+ldi8^-ky5g>)iLlhl^DIy=$DdapUB-TV#%}N?ZJBQn*I;GMA&#ZL2<9^tlvo z9X4nBnwxgzd-8s~Tl(kKj;7t;*V+Ta=>LCa2GkJr`Lu1rOJK;=0A=}sbVzD(iA!Qh zqF!EUaY1Tw%mi=0BL)I4-#zXe{3KT{RL|mc;$)71`U9qEcPB?Q+ZUB5ygSyUT*zuO zlf_l%b!q6yb@SKf*IKlfe{5{6>DrqUs?6#(drp6{e!AT~+mnY2suyqGsBXMy#W#<= zb7p)tX1K}OtSGuixAW4(slJTs?%iUDyP186RGH)tuJG*Grx4a7H zN7K8~O+q$4`kLsx+NyMY;^fy?&)6(=@(zk=ZR1@h*PhaSzNS-mpX`MV`~R^TxrLj5 z*fBN4YMM#5e}IGkLa^DxoTODT9Z6fj@`Z`w;(Zl`nM|w_iq=OairqlhO*zT7ajxODs_5Y4 z>pom;rX~w6ZtO|Zc^S!adq+KQ;LDS9gKe*t%D(+{qW6+X^8C-&>&rJbw((CbJF&H4 z^6K&(8rD)?R~;iIZx@spI-fo|ZT-UunTMnt^el_YUgX^PwpMjvv=f(R+94+QF$Lb58&HgzeME*n|IU#Ou6e zUcE10SqDs|kaWz*B*K8ZEC4E-ti!D&4~jbUR1TDb0*A@D-11C&UQh1hmSzfLn!Jul zQYXM0&Ey*2J=cKhF9IW97^V?Se`25ffm=}lC3P?{Fa)9~`pGdlkW+_nf~n;ZXS>V= G(h2}E`5Za`