From 9eb796515edde6b324eafa112d21cb5854330e37 Mon Sep 17 00:00:00 2001
From: Joel Verhagen
Date: Fri, 16 Jul 2021 12:58:50 -0700
Subject: [PATCH 01/15] Move package query logic to AdminControllerBase
---
.../Admin/Controllers/AdminControllerBase.cs | 70 +++++++
.../Admin/Controllers/DeleteController.cs | 51 +-----
.../Controllers/AdminControllerBaseFacts.cs | 173 ++++++++++++++++++
.../NuGetGallery.Facts.csproj | 1 +
4 files changed, 246 insertions(+), 49 deletions(-)
create mode 100644 tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
index 8ec479a7db..fed5e77542 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
@@ -1,6 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using NuGet.Services.Entities;
+using NuGet.Versioning;
using NuGetGallery.Filters;
namespace NuGetGallery.Areas.Admin.Controllers
@@ -8,5 +13,70 @@ namespace NuGetGallery.Areas.Admin.Controllers
[UIAuthorize(Roles="Admins")]
public class AdminControllerBase : AppController
{
+ internal List SearchForPackages(IPackageService packageService, string query)
+ {
+ // Search suports several options:
+ // 1) Full package id (e.g. jQuery)
+ // 2) Full package id + version (e.g. jQuery 1.9.0, jQuery/1.9.0)
+ // 3) Any of the above separated by comma
+ // We are not using Lucene index here as we want to have the database values.
+
+ var queryParts = query.Split(new[] { ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
+
+ var packages = new List();
+ var completedQueries = new HashSet(StringComparer.OrdinalIgnoreCase);
+ foreach (var queryPart in queryParts)
+ {
+ var spitQuery = queryPart.Split(new[] { ' ', '/' }, StringSplitOptions.RemoveEmptyEntries);
+ if (spitQuery.Length == 1)
+ {
+ // Don't make the same query twice.
+ var id = spitQuery[0].Trim();
+ if (!completedQueries.Add(id))
+ {
+ continue;
+ }
+
+ var resultingRegistration = packageService.FindPackageRegistrationById(id);
+ if (resultingRegistration != null)
+ {
+ packages.AddRange(resultingRegistration
+ .Packages
+ .OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
+ }
+ }
+ else if (spitQuery.Length == 2)
+ {
+ // Don't make the same query twice.
+ var id = spitQuery[0].Trim();
+ var version = spitQuery[1].Trim();
+ if (!completedQueries.Add(id + "/" + version))
+ {
+ continue;
+ }
+
+ var resultingPackage = packageService.FindPackageByIdAndVersionStrict(id, version);
+ if (resultingPackage != null)
+ {
+ packages.Add(resultingPackage);
+ }
+ }
+ }
+
+ // Ensure only unique package instances are returned.
+ var uniquePackagesKeys = new HashSet();
+ var uniquePackages = new List();
+ foreach (var package in packages)
+ {
+ if (!uniquePackagesKeys.Add(package.Key))
+ {
+ continue;
+ }
+
+ uniquePackages.Add(package);
+ }
+
+ return uniquePackages;
+ }
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
index 63408679a9..8d262b5408 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
@@ -12,14 +12,12 @@
namespace NuGetGallery.Areas.Admin.Controllers
{
- public partial class DeleteController : AdminControllerBase
+ public class DeleteController : AdminControllerBase
{
private readonly IPackageService _packageService;
private readonly IPackageDeleteService _packageDeleteService;
private readonly ITelemetryService _telemetryService;
- protected DeleteController() { }
-
public DeleteController(
IPackageService packageService,
IPackageDeleteService packageDeleteService,
@@ -50,55 +48,10 @@ public virtual ActionResult Index()
[HttpGet]
public virtual ActionResult Search(string query)
{
- // Search suports several options:
- // 1) Full package id (e.g. jQuery)
- // 2) Full package id + version (e.g. jQuery 1.9.0)
- // 3) Any of the above separated by comma
- // We are not using Lucene index here as we want to have the database values.
-
- var queryParts = query.Split(new[] { ',', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
-
- var packages = new List();
- var completedQueryParts = new HashSet(StringComparer.OrdinalIgnoreCase);
- foreach (var queryPart in queryParts)
- {
- // Don't make the same query twice.
- if (!completedQueryParts.Add(queryPart.Trim()))
- {
- continue;
- }
-
- var splitQueryPart = queryPart.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries);
- if (splitQueryPart.Length == 1)
- {
- var resultingRegistration = _packageService.FindPackageRegistrationById(splitQueryPart[0].Trim());
- if (resultingRegistration != null)
- {
- packages.AddRange(resultingRegistration
- .Packages
- .OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
- }
- }
- else if (splitQueryPart.Length == 2)
- {
- var resultingPackage = _packageService.FindPackageByIdAndVersionStrict(splitQueryPart[0].Trim(), splitQueryPart[1].Trim());
- if (resultingPackage != null)
- {
- packages.Add(resultingPackage);
- }
- }
- }
-
- // Filter out duplicate packages and create the view model.
- var uniquePackagesKeys = new HashSet();
+ var packages = SearchForPackages(_packageService, query);
var results = new List();
foreach (var package in packages)
{
- if (!uniquePackagesKeys.Add(package.Key))
- {
- continue;
- }
-
results.Add(CreateDeleteSearchResult(package));
}
diff --git a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs
new file mode 100644
index 0000000000..e9cf51a1d4
--- /dev/null
+++ b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs
@@ -0,0 +1,173 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Moq;
+using NuGet.Services.Entities;
+using Xunit;
+
+namespace NuGetGallery.Areas.Admin.Controllers
+{
+ public class AdminControllerBaseFacts
+ {
+ public class TheSearchForPackagesMethod : FactsBase
+ {
+ [Fact]
+ public void DoesNotMakeDuplicateIdQueries()
+ {
+ // Arrange
+ Query = "NuGet.Versioning" + Environment.NewLine + " NUGET.VERSIONING ";
+
+ // Act
+ var result = Target.SearchForPackages(PackageService.Object, Query);
+
+ // Assert
+ PackageService.Verify(
+ x => x.FindPackageRegistrationById("NuGet.Versioning"),
+ Times.Once);
+ }
+
+ [Fact]
+ public void DoesNotMakeDuplicateVersionQueries()
+ {
+ // Arrange
+ Query = "NuGet.Versioning 4.3.0" + Environment.NewLine + " NUGET.VERSIONING 4.3.0 ";
+
+ // Act
+ var result = Target.SearchForPackages(PackageService.Object, Query);
+
+ // Assert
+ PackageService.Verify(
+ x => x.FindPackageByIdAndVersionStrict("NuGet.Versioning", "4.3.0"),
+ Times.Once);
+ }
+
+ [Fact]
+ public void CanSearchForSpecificVersionSplitBySlash()
+ {
+ // Arrange
+ Query = "NuGet.Versioning/4.3.0\nNuGet.VERSIONING/4.4.0";
+
+ // Act
+ var result = Target.SearchForPackages(PackageService.Object, Query);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ PackageService.Verify(
+ x => x.FindPackageByIdAndVersionStrict("NuGet.Versioning", "4.3.0"),
+ Times.Once);
+ PackageService.Verify(
+ x => x.FindPackageByIdAndVersionStrict("NuGet.VERSIONING", "4.4.0"),
+ Times.Once);
+ }
+
+ [Fact]
+ public void SplitsQueriesByComma()
+ {
+ // Arrange
+ Query = "NuGet.Versioning/4.3.0,NuGet.Versioning/4.4.0";
+
+ // Act
+ var result = Target.SearchForPackages(PackageService.Object, Query);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ PackageService.Verify(
+ x => x.FindPackageByIdAndVersionStrict("NuGet.Versioning", "4.3.0"),
+ Times.Once);
+ PackageService.Verify(
+ x => x.FindPackageByIdAndVersionStrict("NuGet.Versioning", "4.4.0"),
+ Times.Once);
+ }
+
+ [Fact]
+ public void DoesNotReturnDuplicatePackages()
+ {
+ // Arrange
+ Packages.Add(Packages[0]);
+
+ // Act
+ var result = Target.SearchForPackages(PackageService.Object, Query);
+
+ // Assert
+ Assert.Equal(2, result.Count);
+ Assert.Equal("4.3.0", result[0].NormalizedVersion);
+ Assert.Equal("4.4.0", result[1].NormalizedVersion);
+ }
+
+ [Fact]
+ public void UsesVersionSpecificIdIfAvailable()
+ {
+ // Arrange
+ Packages[0].Id = "nuget.versioning";
+
+ // Act
+ var result = Target.SearchForPackages(PackageService.Object, Query);
+
+ // Assert
+ Assert.Equal("NuGet.Versioning", result[0].PackageRegistration.Id);
+ Assert.Equal("4.3.0", result[0].NormalizedVersion);
+ Assert.Equal("NuGet.Versioning", result[1].PackageRegistration.Id);
+ Assert.Equal("4.4.0", result[1].NormalizedVersion);
+ }
+ }
+
+ public class FactsBase
+ {
+ public FactsBase()
+ {
+ PackageService = new Mock();
+
+ Query = "NuGet.Versioning";
+ var packageRegistration = new PackageRegistration
+ {
+ Id = "NuGet.Versioning",
+ Owners = new[]
+ {
+ new User { Username = "microsoft" },
+ new User { Username = "nuget" },
+ }
+ };
+ Packages = new List()
+ {
+ new Package
+ {
+ Key = 2,
+ PackageRegistration = packageRegistration,
+ NormalizedVersion = "4.4.0",
+ },
+ new Package
+ {
+ Key = 1,
+ PackageRegistration = packageRegistration,
+ NormalizedVersion = "4.3.0",
+ },
+ };
+
+ PackageService
+ .Setup(x => x.FindPackageRegistrationById(It.IsAny()))
+ .Returns(x => new PackageRegistration
+ {
+ Packages = Packages.Where(y => y.PackageRegistration.Id.Equals(x, StringComparison.OrdinalIgnoreCase)).ToList(),
+ });
+
+ PackageService
+ .Setup(x => x.FindPackageByIdAndVersionStrict(It.IsAny(), It.IsAny()))
+ .Returns((i, v) => Packages
+ .Where(x =>
+ x.PackageRegistration.Id.Equals(i, StringComparison.OrdinalIgnoreCase) &&
+ x.NormalizedVersion.Equals(NuGetVersionFormatter.Normalize(v), StringComparison.OrdinalIgnoreCase))
+ .FirstOrDefault());
+
+ Target = new AdminControllerBase();
+ }
+
+ public Mock PackageService { get; }
+ public string Query { get; set; }
+ public List Packages { get; }
+ public AdminControllerBase Target { get; }
+ }
+ }
+}
diff --git a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj
index 5a13c85526..dea8fb8720 100644
--- a/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj
+++ b/tests/NuGetGallery.Facts/NuGetGallery.Facts.csproj
@@ -63,6 +63,7 @@
+
From b7b6ae1ed9c693f46aaaf1ac225632e0520c3967 Mon Sep 17 00:00:00 2001
From: Joel Verhagen
Date: Fri, 16 Jul 2021 13:19:56 -0700
Subject: [PATCH 02/15] Refactor DeleteSearchResult to PackageSearchResult
---
.../Admin/Controllers/AdminControllerBase.cs | 27 ++++++++++
.../Admin/Controllers/DeleteController.cs | 53 +++++++++----------
...SearchResult.cs => PackageSearchResult.cs} | 2 +-
src/NuGetGallery/NuGetGallery.csproj | 2 +-
.../Controllers/AdminControllerBaseFacts.cs | 46 +++++++++++++++-
.../Controllers/DeleteControllerFacts.cs | 4 +-
6 files changed, 101 insertions(+), 33 deletions(-)
rename src/NuGetGallery/Areas/Admin/ViewModels/{DeleteSearchResult.cs => PackageSearchResult.cs} (94%)
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
index fed5e77542..b7a3be4054 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
@@ -6,6 +6,7 @@
using System.Linq;
using NuGet.Services.Entities;
using NuGet.Versioning;
+using NuGetGallery.Areas.Admin.ViewModels;
using NuGetGallery.Filters;
namespace NuGetGallery.Areas.Admin.Controllers
@@ -78,5 +79,31 @@ internal List SearchForPackages(IPackageService packageService, string
return uniquePackages;
}
+
+ internal PackageSearchResult CreatePackageSearchResult(Package package)
+ {
+ return new PackageSearchResult
+ {
+ PackageId = package.Id,
+ PackageVersionNormalized = !string.IsNullOrEmpty(package.NormalizedVersion)
+ ? package.NormalizedVersion
+ : NuGetVersion.Parse(package.Version).ToNormalizedString(),
+ DownloadCount = package.DownloadCount,
+ Created = package.Created.ToNuGetShortDateString(),
+ Listed = package.Listed,
+ PackageStatus = package.PackageStatusKey.ToString(),
+ Owners = package
+ .PackageRegistration
+ .Owners
+ .Select(u => u.Username)
+ .OrderBy(u => u, StringComparer.OrdinalIgnoreCase)
+ .Select(username => new UserViewModel
+ {
+ Username = username,
+ ProfileUrl = Url.User(username),
+ })
+ .ToList()
+ };
+ }
}
}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
index 8d262b5408..693a20d390 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
@@ -12,6 +12,29 @@
namespace NuGetGallery.Areas.Admin.Controllers
{
+ public class UnlistController : AdminControllerBase
+ {
+ private readonly IPackageService _packageService;
+
+ public UnlistController(IPackageService packageService)
+ {
+ _packageService = packageService;
+ }
+
+ [HttpGet]
+ public virtual ActionResult Search(string query)
+ {
+ var packages = SearchForPackages(_packageService, query);
+ var results = new List();
+ foreach (var package in packages)
+ {
+ results.Add(CreatePackageSearchResult(package));
+ }
+
+ return Json(results, JsonRequestBehavior.AllowGet);
+ }
+ }
+
public class DeleteController : AdminControllerBase
{
private readonly IPackageService _packageService;
@@ -49,41 +72,15 @@ public virtual ActionResult Index()
public virtual ActionResult Search(string query)
{
var packages = SearchForPackages(_packageService, query);
- var results = new List();
+ var results = new List();
foreach (var package in packages)
{
- results.Add(CreateDeleteSearchResult(package));
+ results.Add(CreatePackageSearchResult(package));
}
return Json(results, JsonRequestBehavior.AllowGet);
}
- private DeleteSearchResult CreateDeleteSearchResult(Package package)
- {
- return new DeleteSearchResult
- {
- PackageId = package.Id,
- PackageVersionNormalized = !string.IsNullOrEmpty(package.NormalizedVersion)
- ? package.NormalizedVersion
- : NuGetVersion.Parse(package.Version).ToNormalizedString(),
- DownloadCount = package.DownloadCount,
- Created = package.Created.ToNuGetShortDateString(),
- Listed = package.Listed,
- PackageStatus = package.PackageStatusKey.ToString(),
- Owners = package
- .PackageRegistration
- .Owners
- .Select(u => u.Username)
- .OrderBy(u => u)
- .Select(username => new UserViewModel
- {
- Username = username,
- ProfileUrl = Url.User(username),
- })
- .ToList()
- };
- }
-
[HttpGet]
public virtual ActionResult Reflow()
{
diff --git a/src/NuGetGallery/Areas/Admin/ViewModels/DeleteSearchResult.cs b/src/NuGetGallery/Areas/Admin/ViewModels/PackageSearchResult.cs
similarity index 94%
rename from src/NuGetGallery/Areas/Admin/ViewModels/DeleteSearchResult.cs
rename to src/NuGetGallery/Areas/Admin/ViewModels/PackageSearchResult.cs
index f9f6eeb573..4ca15c87f3 100644
--- a/src/NuGetGallery/Areas/Admin/ViewModels/DeleteSearchResult.cs
+++ b/src/NuGetGallery/Areas/Admin/ViewModels/PackageSearchResult.cs
@@ -5,7 +5,7 @@
namespace NuGetGallery.Areas.Admin.ViewModels
{
- public class DeleteSearchResult
+ public class PackageSearchResult
{
public string PackageId { get; set; }
public string PackageVersionNormalized { get; set; }
diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj
index fc70273b6d..5dad1a6813 100644
--- a/src/NuGetGallery/NuGetGallery.csproj
+++ b/src/NuGetGallery/NuGetGallery.csproj
@@ -195,7 +195,7 @@
-
+
diff --git a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs
index e9cf51a1d4..238f992c36 100644
--- a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs
+++ b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/AdminControllerBaseFacts.cs
@@ -4,14 +4,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Web;
using Moq;
using NuGet.Services.Entities;
+using NuGetGallery.Framework;
using Xunit;
namespace NuGetGallery.Areas.Admin.Controllers
{
public class AdminControllerBaseFacts
{
+ public class TheCreatePackageSearchResultMethod : FactsBase
+ {
+ [Fact]
+ public void MapsExpectedProperties()
+ {
+ var package = new Package
+ {
+ Id = "Newtonsoft.Json",
+ NormalizedVersion = "9.0.1",
+ DownloadCount = 1337,
+ Created = new DateTime(2021, 7, 6, 13, 5, 30),
+ Listed = true,
+ PackageStatusKey = PackageStatus.Available,
+ PackageRegistration = new PackageRegistration
+ {
+ Owners = new List
+ {
+ new User { Username = "gates" },
+ new User { Username = "bill" },
+ },
+ }
+ };
+
+ var result = Target.CreatePackageSearchResult(package);
+
+ Assert.Equal("Newtonsoft.Json", result.PackageId);
+ Assert.Equal("9.0.1", result.PackageVersionNormalized);
+ Assert.Equal(1337, result.DownloadCount);
+ Assert.Equal(package.Created.ToNuGetShortDateString(), result.Created);
+ Assert.True(result.Listed);
+ Assert.Equal("Available", result.PackageStatus);
+ Assert.Equal(2, result.Owners.Count);
+ Assert.Equal("bill", result.Owners[0].Username);
+ Assert.Equal("/profiles/bill", result.Owners[0].ProfileUrl);
+ Assert.Equal("gates", result.Owners[1].Username);
+ Assert.Equal("/profiles/gates", result.Owners[1].ProfileUrl);
+ }
+ }
+
public class TheSearchForPackagesMethod : FactsBase
{
[Fact]
@@ -114,7 +155,7 @@ public void UsesVersionSpecificIdIfAvailable()
}
}
- public class FactsBase
+ public class FactsBase : TestContainer
{
public FactsBase()
{
@@ -162,6 +203,9 @@ public FactsBase()
.FirstOrDefault());
Target = new AdminControllerBase();
+
+ var httpContextBase = new Mock();
+ TestUtility.SetupHttpContextMockForUrlGeneration(httpContextBase, Target);
}
public Mock PackageService { get; }
diff --git a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs
index 572ed8e00c..023fc8e54b 100644
--- a/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs
+++ b/tests/NuGetGallery.Facts/Areas/Admin/Controllers/DeleteControllerFacts.cs
@@ -44,7 +44,7 @@ public void DoesNotReturnDuplicatePackages()
// Assert
var jsonResult = Assert.IsType(result);
- var searchResults = Assert.IsType>(jsonResult.Data);
+ var searchResults = Assert.IsType>(jsonResult.Data);
var uniqueIdentities = searchResults.Select(p => $"{p.PackageId} {p.PackageVersionNormalized}").Distinct();
Assert.Equal(searchResults.Count, uniqueIdentities.Count());
}
@@ -60,7 +60,7 @@ public void UsesVersionSpecificIdIfAvailable()
// Assert
var jsonResult = Assert.IsType(result);
- var searchResults = Assert.IsType>(jsonResult.Data);
+ var searchResults = Assert.IsType>(jsonResult.Data);
Assert.Equal("NuGet.Versioning", searchResults[0].PackageId);
Assert.Equal("4.3.0", searchResults[0].PackageVersionNormalized);
Assert.Equal("nuget.versioning", searchResults[1].PackageId);
From ab061c7b6f303cdfe225af7c285177786a1b105d Mon Sep 17 00:00:00 2001
From: Joel Verhagen
Date: Fri, 16 Jul 2021 17:04:07 -0700
Subject: [PATCH 03/15] Add bulk listed update service using same logic as
deprecation
---
.../Gallery/ThrowingTelemetryService.cs | 5 ++
.../IPackageUpdateService.cs | 9 +++
.../PackageManagement/PackageUpdateService.cs | 67 ++++++++++++++++++-
.../Telemetry/ITelemetryService.cs | 2 +
.../Telemetry/TelemetryService.cs | 15 +++++
.../Fakes/FakeTelemetryService.cs | 5 ++
6 files changed, 101 insertions(+), 2 deletions(-)
diff --git a/src/GitHubVulnerabilities2Db/Gallery/ThrowingTelemetryService.cs b/src/GitHubVulnerabilities2Db/Gallery/ThrowingTelemetryService.cs
index 9d95eb5a21..2da9bde11f 100644
--- a/src/GitHubVulnerabilities2Db/Gallery/ThrowingTelemetryService.cs
+++ b/src/GitHubVulnerabilities2Db/Gallery/ThrowingTelemetryService.cs
@@ -291,6 +291,11 @@ public void TrackPackageRevalidate(Package package)
throw new NotImplementedException();
}
+ public void TrackPackagesUpdateListed(IReadOnlyList packages, bool listed)
+ {
+ throw new NotImplementedException();
+ }
+
public void TrackPackageUnlisted(Package package)
{
throw new NotImplementedException();
diff --git a/src/NuGetGallery.Services/PackageManagement/IPackageUpdateService.cs b/src/NuGetGallery.Services/PackageManagement/IPackageUpdateService.cs
index dd1e3552a2..f151ed9d3b 100644
--- a/src/NuGetGallery.Services/PackageManagement/IPackageUpdateService.cs
+++ b/src/NuGetGallery.Services/PackageManagement/IPackageUpdateService.cs
@@ -17,6 +17,15 @@ public interface IPackageUpdateService
/// If true, will be called.
Task MarkPackageUnlistedAsync(Package package, bool commitChanges = true, bool updateIndex = true);
+ ///
+ /// Updates the listed status on a batch of packages. All of the packages must be related to the same package registration.
+ /// Packages that are deleted or have failed validation are not allowed. Packages that already have a matching listed state
+ /// will be ignored.
+ ///
+ /// The packages to update.
+ /// True to make the packages listed, false to make the packages unlisted.
+ Task UpdateListedInBulkAsync(IReadOnlyList packages, bool listed);
+
///
/// Marks as listed.
///
diff --git a/src/NuGetGallery.Services/PackageManagement/PackageUpdateService.cs b/src/NuGetGallery.Services/PackageManagement/PackageUpdateService.cs
index 78bde7c665..b258453829 100644
--- a/src/NuGetGallery.Services/PackageManagement/PackageUpdateService.cs
+++ b/src/NuGetGallery.Services/PackageManagement/PackageUpdateService.cs
@@ -32,6 +32,69 @@ public PackageUpdateService(
_indexingService = indexingService ?? throw new ArgumentNullException(nameof(indexingService));
}
+ public async Task UpdateListedInBulkAsync(IReadOnlyList packages, bool listed)
+ {
+ if (packages == null || !packages.Any())
+ {
+ throw new ArgumentException("At least one package must be provided.");
+ }
+
+ foreach (var package in packages)
+ {
+ if (package.PackageStatusKey == PackageStatus.Deleted)
+ {
+ throw new ArgumentException("A deleted package cannot have its listed status changed.");
+ }
+
+ if (package.PackageStatusKey == PackageStatus.FailedValidation)
+ {
+ throw new ArgumentException("A package that failed validation cannot have its listed status changed.");
+ }
+ }
+
+ var registration = packages.First().PackageRegistration;
+ if (packages.Select(p => p.PackageRegistrationKey).Distinct().Count() > 1)
+ {
+ throw new ArgumentException("All packages to change the listing status of must have the same ID.", nameof(packages));
+ }
+
+ using (var strategy = new SuspendDbExecutionStrategy())
+ using (var transaction = _entitiesContext.GetDatabase().BeginTransaction())
+ {
+ var updatedPackages = new List();
+ foreach (var package in packages)
+ {
+ if (package.Listed != listed)
+ {
+ package.Listed = listed;
+ updatedPackages.Add(package);
+ }
+ }
+
+ if (updatedPackages.Any())
+ {
+ await _packageService.UpdateIsLatestAsync(registration, commitChanges: false);
+
+ await _entitiesContext.SaveChangesAsync();
+
+ await UpdatePackagesAsync(updatedPackages);
+
+ transaction.Commit();
+
+ _telemetryService.TrackPackagesUpdateListed(updatedPackages, listed);
+
+ foreach (var package in updatedPackages)
+ {
+ await _auditingService.SaveAuditRecordAsync(new PackageAuditRecord(
+ package,
+ listed ? AuditedPackageAction.List : AuditedPackageAction.Unlist));
+
+ _indexingService.UpdatePackage(package);
+ }
+ }
+ }
+ }
+
public async Task MarkPackageListedAsync(Package package, bool commitChanges = true, bool updateIndex = true)
{
if (package == null)
@@ -46,12 +109,12 @@ public async Task MarkPackageListedAsync(Package package, bool commitChanges = t
if (package.PackageStatusKey == PackageStatus.Deleted)
{
- throw new InvalidOperationException("A deleted package should never be listed!");
+ throw new InvalidOperationException("A deleted package should never be listed.");
}
if (package.PackageStatusKey == PackageStatus.FailedValidation)
{
- throw new InvalidOperationException("A package that failed validation should never be listed!");
+ throw new InvalidOperationException("A package that failed validation should never be listed.");
}
package.Listed = true;
diff --git a/src/NuGetGallery.Services/Telemetry/ITelemetryService.cs b/src/NuGetGallery.Services/Telemetry/ITelemetryService.cs
index fafe720fdb..10cb497de0 100644
--- a/src/NuGetGallery.Services/Telemetry/ITelemetryService.cs
+++ b/src/NuGetGallery.Services/Telemetry/ITelemetryService.cs
@@ -38,6 +38,8 @@ public interface ITelemetryService
void TrackPackageListed(Package package);
+ void TrackPackagesUpdateListed(IReadOnlyList packages, bool listed);
+
void TrackPackageDelete(Package package, bool isHardDelete);
void TrackPackageReupload(Package package);
diff --git a/src/NuGetGallery.Services/Telemetry/TelemetryService.cs b/src/NuGetGallery.Services/Telemetry/TelemetryService.cs
index beafc5edde..1ae855cae6 100644
--- a/src/NuGetGallery.Services/Telemetry/TelemetryService.cs
+++ b/src/NuGetGallery.Services/Telemetry/TelemetryService.cs
@@ -46,6 +46,7 @@ public class Events
public const string PackageReflow = "PackageReflow";
public const string PackageUnlisted = "PackageUnlisted";
public const string PackageListed = "PackageListed";
+ public const string PackageUpdateListed = "PackageUpdateListed";
public const string PackageDelete = "PackageDelete";
public const string PackageDeprecate = "PackageDeprecate";
public const string PackageReupload = "PackageReupload";
@@ -131,6 +132,9 @@ public class Events
public const string PackageVersion = "PackageVersion";
public const string PackageVersions = "PackageVersions";
+ // Package listed properties
+ public const string Listed = "Listed";
+
// Package deprecate properties
public const string DeprecationReason = "PackageDeprecationReason";
public const string DeprecationAlternatePackageId = "PackageDeprecationAlternatePackageId";
@@ -460,6 +464,17 @@ public void TrackPackageListed(Package package)
TrackMetricForPackage(Events.PackageListed, package);
}
+ public void TrackPackagesUpdateListed(IReadOnlyList packages, bool listed)
+ {
+ TrackMetricForPackageVersions(
+ Events.PackageUpdateListed,
+ packages,
+ properties =>
+ {
+ properties.Add(Listed, listed.ToString());
+ });
+ }
+
public void TrackPackageDelete(Package package, bool isHardDelete)
{
TrackMetricForPackage(Events.PackageDelete, package, properties =>
diff --git a/src/VerifyMicrosoftPackage/Fakes/FakeTelemetryService.cs b/src/VerifyMicrosoftPackage/Fakes/FakeTelemetryService.cs
index b729fcda84..043379f89b 100644
--- a/src/VerifyMicrosoftPackage/Fakes/FakeTelemetryService.cs
+++ b/src/VerifyMicrosoftPackage/Fakes/FakeTelemetryService.cs
@@ -288,6 +288,11 @@ public void TrackPackageRevalidate(Package package)
throw new NotImplementedException();
}
+ public void TrackPackagesUpdateListed(IReadOnlyList packages, bool listed)
+ {
+ throw new NotImplementedException();
+ }
+
public void TrackPackageUnlisted(Package package)
{
throw new NotImplementedException();
From 0cd423ef248654eb3dd81f12b1552fb16b4ee89f Mon Sep 17 00:00:00 2001
From: Joel Verhagen
Date: Fri, 16 Jul 2021 17:04:45 -0700
Subject: [PATCH 04/15] Add UpdateListedController
---
.../Admin/Controllers/AdminControllerBase.cs | 8 +-
.../Admin/Controllers/DeleteController.cs | 23 ---
.../Controllers/UpdateListedController.cs | 91 ++++++++++++
.../Admin/Views/UpdateListed/Index.cshtml | 140 ++++++++++++++++++
src/NuGetGallery/NuGetGallery.csproj | 5 +
.../RequestModels/UpdateListedRequest.cs | 21 +++
6 files changed, 258 insertions(+), 30 deletions(-)
create mode 100644 src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs
create mode 100644 src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml
create mode 100644 src/NuGetGallery/RequestModels/UpdateListedRequest.cs
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
index b7a3be4054..40ecc331d5 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
@@ -38,13 +38,7 @@ internal List SearchForPackages(IPackageService packageService, string
continue;
}
- var resultingRegistration = packageService.FindPackageRegistrationById(id);
- if (resultingRegistration != null)
- {
- packages.AddRange(resultingRegistration
- .Packages
- .OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
- }
+ packages.AddRange(packageService.FindPackagesById(id).OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
}
else if (spitQuery.Length == 2)
{
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
index 693a20d390..e7f6f7105a 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs
@@ -12,29 +12,6 @@
namespace NuGetGallery.Areas.Admin.Controllers
{
- public class UnlistController : AdminControllerBase
- {
- private readonly IPackageService _packageService;
-
- public UnlistController(IPackageService packageService)
- {
- _packageService = packageService;
- }
-
- [HttpGet]
- public virtual ActionResult Search(string query)
- {
- var packages = SearchForPackages(_packageService, query);
- var results = new List();
- foreach (var package in packages)
- {
- results.Add(CreatePackageSearchResult(package));
- }
-
- return Json(results, JsonRequestBehavior.AllowGet);
- }
- }
-
public class DeleteController : AdminControllerBase
{
private readonly IPackageService _packageService;
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs b/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs
new file mode 100644
index 0000000000..8c92b4c5e2
--- /dev/null
+++ b/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs
@@ -0,0 +1,91 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Web.Mvc;
+using NuGet.Services.Entities;
+using NuGetGallery.Areas.Admin.ViewModels;
+
+namespace NuGetGallery.Areas.Admin.Controllers
+{
+ public class UpdateListedController : AdminControllerBase
+ {
+ private readonly IPackageService _packageService;
+ private readonly IPackageUpdateService _packageUpdateService;
+
+ public UpdateListedController(
+ IPackageService packageService,
+ IPackageUpdateService packageUpdateService)
+ {
+ _packageService = packageService;
+ _packageUpdateService = packageUpdateService;
+ }
+
+ [HttpGet]
+ public virtual ActionResult Index()
+ {
+ return View(new UpdateListedRequest());
+ }
+
+ [HttpGet]
+ public virtual ActionResult Search(string query)
+ {
+ var packages = SearchForPackages(_packageService, query);
+ var results = new List();
+ foreach (var package in packages)
+ {
+ results.Add(CreatePackageSearchResult(package));
+ }
+
+ return Json(results, JsonRequestBehavior.AllowGet);
+ }
+
+ [HttpPost]
+ [ValidateAntiForgeryToken]
+ public async Task UpdateListed(UpdateListedRequest updateListed)
+ {
+ if (ModelState.IsValid)
+ {
+ var idToVersions = updateListed
+ .Packages
+ .Select(x => x.Split(new[] { '|' }, StringSplitOptions.RemoveEmptyEntries))
+ .Where(x => x.Length == 2)
+ .GroupBy(x => x[0], x => x[1], StringComparer.OrdinalIgnoreCase);
+
+ var packageRegistrationCount = 0;
+ var packageCount = 0;
+ var noOpCount = 0;
+ foreach (var group in idToVersions)
+ {
+ var normalizedVersions = group
+ .Select(x => NuGetVersionFormatter.Normalize(x))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ var packages = _packageService
+ .FindPackagesById(group.Key, PackageDeprecationFieldsToInclude.DeprecationAndRelationships)
+ .Where(x => normalizedVersions.Contains(x.NormalizedVersion))
+ .Where(x => x.Listed != updateListed.Listed)
+ .ToList();
+
+ packageRegistrationCount++;
+ packageCount += packages.Count;
+ noOpCount += normalizedVersions.Count - packages.Count;
+
+ await _packageUpdateService.UpdateListedInBulkAsync(packages, updateListed.Listed);
+ }
+
+ TempData["Message"] = $"{packageCount} packages across {packageRegistrationCount} package IDs have " +
+ $"been {(updateListed.Listed ? "relisted" : "unlisted")}. {noOpCount} packages were already " +
+ $"up-to-date and were left unchanged.";
+ return RedirectToAction(nameof(Index));
+ }
+ else
+ {
+ TempData["Message"] = "The provided input is not valid.";
+ return RedirectToAction(nameof(Index));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml
new file mode 100644
index 0000000000..b0f350bf5f
--- /dev/null
+++ b/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml
@@ -0,0 +1,140 @@
+@model NuGetGallery.UpdateListedRequest
+
+@{
+ ViewBag.Title = "Unlist/relist packages";
+}
+
+
+ Unlist/relist packages
+
+
+
+ @using (Html.BeginForm("UpdateListed", "UpdateListed", new { area = "Admin" }, FormMethod.Post, new { id = "update-listed-form" }))
+ {
+ @Html.AntiForgeryToken()
+
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+@section BottomScripts {
+
+}
\ No newline at end of file
diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj
index 5dad1a6813..70aec32b11 100644
--- a/src/NuGetGallery/NuGetGallery.csproj
+++ b/src/NuGetGallery/NuGetGallery.csproj
@@ -143,6 +143,7 @@
+
Url_Edit.ascx
@@ -310,6 +311,7 @@
+
@@ -2319,6 +2321,9 @@
+
+
+
10.0
$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
diff --git a/src/NuGetGallery/RequestModels/UpdateListedRequest.cs b/src/NuGetGallery/RequestModels/UpdateListedRequest.cs
new file mode 100644
index 0000000000..dfa9b0921d
--- /dev/null
+++ b/src/NuGetGallery/RequestModels/UpdateListedRequest.cs
@@ -0,0 +1,21 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using NuGetGallery.Infrastructure;
+
+namespace NuGetGallery
+{
+ public class UpdateListedRequest
+ {
+ public UpdateListedRequest()
+ {
+ Packages = new List();
+ }
+
+ public List Packages { get; set; }
+
+ public bool Listed { get; set; }
+ }
+}
From 977de8dbf7fb3b9bf711da865958fec760d9c3e5 Mon Sep 17 00:00:00 2001
From: Joel Verhagen
Date: Fri, 16 Jul 2021 17:16:17 -0700
Subject: [PATCH 05/15] Add the new link to admin home, fix a bug
---
.../Controllers/UpdateListedController.cs | 9 ++++++--
.../Areas/Admin/Views/Home/Index.cshtml | 23 ++++++++++++++-----
.../Admin/Views/UpdateListed/Index.cshtml | 2 +-
3 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs b/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs
index 8c92b4c5e2..8f11f23a99 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/UpdateListedController.cs
@@ -67,13 +67,18 @@ public async Task UpdateListed(UpdateListedRequest updateListed)
.FindPackagesById(group.Key, PackageDeprecationFieldsToInclude.DeprecationAndRelationships)
.Where(x => normalizedVersions.Contains(x.NormalizedVersion))
.Where(x => x.Listed != updateListed.Listed)
+ .Where(x => x.PackageStatusKey != PackageStatus.Deleted)
+ .Where(x => x.PackageStatusKey != PackageStatus.FailedValidation)
.ToList();
- packageRegistrationCount++;
packageCount += packages.Count;
noOpCount += normalizedVersions.Count - packages.Count;
- await _packageUpdateService.UpdateListedInBulkAsync(packages, updateListed.Listed);
+ if (packages.Any())
+ {
+ packageRegistrationCount++;
+ await _packageUpdateService.UpdateListedInBulkAsync(packages, updateListed.Listed);
+ }
}
TempData["Message"] = $"{packageCount} packages across {packageRegistrationCount} package IDs have " +
diff --git a/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml
index 25b76c2377..007d82e0d9 100644
--- a/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml
+++ b/src/NuGetGallery/Areas/Admin/Views/Home/Index.cshtml
@@ -54,6 +54,17 @@
Delete packages from the gallery.
+
+
+
+ Unlist or relist many packages at once.
+
+
Clear Content Cache
diff --git a/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml b/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml
index b0f350bf5f..c9d557a52d 100644
--- a/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml
+++ b/src/NuGetGallery/Areas/Admin/Views/UpdateListed/Index.cshtml
@@ -1,7 +1,7 @@
@model NuGetGallery.UpdateListedRequest
@{
- ViewBag.Title = "Unlist/relist packages";
+ ViewBag.Title = "Unlist/relist Packages";
}
From fd1c9c3c1372012872a25d88fd88924066183bcc Mon Sep 17 00:00:00 2001
From: Joel Verhagen
Date: Fri, 16 Jul 2021 17:47:57 -0700
Subject: [PATCH 06/15] Add tests to PackageUpdateServiceFacts and fix broken
tests
---
.../Admin/Controllers/AdminControllerBase.cs | 8 +-
.../Services/PackageUpdateServiceFacts.cs | 127 ++++++++++++++++++
.../Services/TelemetryServiceFacts.cs | 4 +
3 files changed, 138 insertions(+), 1 deletion(-)
diff --git a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
index 40ecc331d5..b7a3be4054 100644
--- a/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
+++ b/src/NuGetGallery/Areas/Admin/Controllers/AdminControllerBase.cs
@@ -38,7 +38,13 @@ internal List SearchForPackages(IPackageService packageService, string
continue;
}
- packages.AddRange(packageService.FindPackagesById(id).OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
+ var resultingRegistration = packageService.FindPackageRegistrationById(id);
+ if (resultingRegistration != null)
+ {
+ packages.AddRange(resultingRegistration
+ .Packages
+ .OrderBy(p => NuGetVersion.Parse(p.NormalizedVersion)));
+ }
}
else if (spitQuery.Length == 2)
{
diff --git a/tests/NuGetGallery.Facts/Services/PackageUpdateServiceFacts.cs b/tests/NuGetGallery.Facts/Services/PackageUpdateServiceFacts.cs
index d5b764982b..2ee93e7033 100644
--- a/tests/NuGetGallery.Facts/Services/PackageUpdateServiceFacts.cs
+++ b/tests/NuGetGallery.Facts/Services/PackageUpdateServiceFacts.cs
@@ -24,6 +24,133 @@ public enum PackageLatestState
LatestStableSemVer2
}
+
+ public class TheUpdateDeprecationMethod : TestContainer
+ {
+ public static IEnumerable